State

Status: Draft

in order to let you grasp the way state is used i'll show you a few things:

  • ways of passing data around
  • intro to redux
  • store
  • reducers
  • dispatch
  • selectors
  • remote dispatch

ways of passing data around

each page needs data

the "tutorial" way is to just fetch data within the page route, which is fine but it has it's downsides if you take the bigger picture (all the ways in which your data will be read & updated throughout the application).

if you've ever built a larger application you realize that an application consists of many small components of which each components needs some piece of information from your database.

managaging this complexity and tying up all the props in react can quickly become a hassle.

if you've experienced that long enough you'll feel the need for some "global data object" to store everything in which can be read from that object.

one way of doing this is using React context.

another way of doing this is using redux.

intro to redux

redux is a fairly simple concept to grasp, and once you've understood it, you'll probably want to use it everywhere.

it consists of three 4 main parts:

  • store -- the object that contains all global data, called state. this state is ideally normalized. (read more on this in concepts → normalization)
  • reducers - the definition of functions that update this state outside of your local components, that way they can be called from anywhere in the application (which is what we often need, think of liking a post in a list of posts or on the post detail page, it's both the same action the user has to take)
  • dispatch - a way to execute a reducer from within a component somewhere deep in your app
  • selectors - a way to map the state to props within the component, it's a function that takes all the information needed for the component to function from the state object

store

example of the store's initial state:

{
    "Post": {
        "post:1": {
            "id": "post:1",
            "name": "Lorem ipsum",
            "upvotes": 1337
        }
    }
}

@/lib/store.js

import { configureStore } from '@reduxjs/toolkit'
import reducer from '@/lib/store/reducer.js'

return configureStore({
    preloadedState: state,
    reducer
})

reducers

@/lib/reducer.js

const reducers = {
    upvotePost: (state, payload) => {

        const { postId } = payload

        const post = state.Post[postId]

        return {
            ...state,
            Post: {
                ...state.Post,
                [postId]: {
                    ...post,
                    upvotes: post.upvotes + 1
                }
            }
        }
    }
}

const reducer = (state, action) => {
    const reduceFn = reducers[action.type]
    if (reduceFn) {
        return reduceFn(state, action.payload)
    }
    return state
}

selectors & dispatch

useSelector

a way to map the state to props within the component, it's a function that aggregates the state into an object that the component needs:

import { useSelector } from 'react-redux'

const { post } = useSelector(state => {

    return {
        post: state.Post[props.postId]
    }
})

useDispatch

a way to execute a reducer from within a component somewhere deep in your app:

import { useDispatch } from 'react-redux'

const handleUpvote = () => dispatch({
    type: 'upvotePost',
    payload: {
        postId: props.postId,
    }
})

full example

import { useSelector, useDispatch } from 'react-redux'

export default function Post(props) {

    const dispatch = useDispatch()

    const { post } = useSelectors(state => {

        return {
            post: state.Post[props.postId]
        }
    })

    const handleUpvote = () => dispatch({
        type: 'upvotePost',
        payload: {
            postId: props.postId,
            upvote
        }
    })

    return (
        <div>
            upvote count: {post.upvotes}
            <button type="button" onClick={handleUpvote}>
                Upvote this post 👍
            </button>
        </div>
    )
}

remote dispatch

Although the standard useDispatch function of redux is great for client-side state management. An application typically has to write updates to the data shown back to the database via the server-side (remote).

There's many ways of doing this and I've settled on a variant of dispatch which I call remoteDispatch.

If you're used to building applications with a <form> submit, this is what we're essentially replicating. Except instead of having to refetch the whole page we just refetch the page state data.

In a nutshell what it does is as follows:

  • user opens page
  • page object gets created based on the page params which contains the global state
  • you call remoteDispatch, just as you would call dispatch, except you make it asynchronous (waiting on a response from the server).
  • the remoteDispatch function will send your request to the server
  • on the server-side you'll define a handler which takes this requests, mutates the data necessary
  • the page object gets recreated and will now have the updated state data.
  • the response comes back to the client which will take the state out of the page object and apply it using dispatch completely replacing the state already in the store.
  • this will cause all components throughout your application that use useSelector to update.

This is what it looks like in action:

import { useState } from 'react'
import { useSelector } from 'react-redux'
import { remoteDispatch } from '@/lib/store/remoteDispatch'

export default function Post(props) {

    const [loading, setLoading] = useState(false)

    const remoteDispatch = useRemoteDispatch()

    const { post } = useSelectors(state => {

        return {
            post: state.Post[props.postId]
        }
    })

    const handleUpvote = async () => {

        setLoading(true)

        await remoteDispatch({
            type: 'upvotePost',
            payload: {
                postId: props.postId,
                upvote
            }
        })

        setLoading(false)
    }

    return (
        <div>
            upvote count: {post.upvotes}
            <button type="button" onClick={handleUpvote}>
                {loading ? 'loading...' : 'Upvote this post 👍'}
            </button>
        </div>
    )
}