DEV Community

loading...
Cover image for Full State Management in React (without Redux)

Full State Management in React (without Redux)

roggc profile image roggc ・7 min read

Motivation

Having an App component having this child:


            <Counterss name={name1} liftUp={catch1}/>

and the Counterss component having these children:


        <Counters liftUp={catch1} name={name+'-'+name1}/>
        <Counters liftUp={catch2} name={name+'-'+name2}/>

and the Counters component having these children:


            <Counter liftUp={catch1}
            name={name+'-'+name1}/>
            <Counter liftUp={catch2}
            name={name+'-'+name2}/>

I want this:
Alt Text
that's it, I want full control of my state. I want each component to have a local state defined through the use of useReducer and I want a store object where I can access all these local states of all the components, starting from the App component to the innermost component, from anywhere in the app, in any component, from the outermost to the innermost.
I want to use useContext to get access to this store object in order to be able to use dispatch and state of any local state of the components of the app anywhere and I want it to be reactive.
For that purpose I need named components, that's it, I must pass a property named name to each component when I use it in the app.
Also, I need what is found in the article lifting up information in React from one component to its parents in the component tree because the strategy I will follow will be to lift up all info, that's it state and dispatch of every local state, and then make it accessible to all the components through a store object defined in the App component with the use of useContext.

The HOC

The HOC I will be using it's a variant of the one defined in the post mentioned above. Because one component can have more than one child, I am interested in catching up all the info of all of the children, so I define the HOC like this:

import React,{useState,useRef} from 'react'

export default C=>(props)=>{
    const [foo,setFoo]=useState(0)
    const info1=useRef(null)
    const catch1=(info)=>{
        info1.current=info
        setFoo(prev=>prev+1)
    }
    const info2=useRef(null)
    const catch2=(info)=>{
        info2.current=info
        setFoo(prev=>prev+1)
    }
    const info3=useRef(null)
    const catch3=(info)=>{
        info3.current=info
        setFoo(prev=>prev+1)
    }
    const info4=useRef(null)
    const catch4=(info)=>{
        info4.current=info
        setFoo(prev=>prev+1)
    }
    const info5=useRef(null)
    const catch5=(info)=>{
        info5.current=info
        setFoo(prev=>prev+1)
    }
    const info6=useRef(null)
    const catch6=(info)=>{
        info6.current=info
        setFoo(prev=>prev+1)
    }
    const info7=useRef(null)
    const catch7=(info)=>{
        info7.current=info
        setFoo(prev=>prev+1)
    }
    const info8=useRef(null)
    const catch8=(info)=>{
        info8.current=info
        setFoo(prev=>prev+1)
    }
    const info9=useRef(null)
    const catch9=(info)=>{
        info9.current=info
        setFoo(prev=>prev+1)
    }
    const info10=useRef(null)
    const catch10=(info)=>{
        info10.current=info
        setFoo(prev=>prev+1)
    }
    return (
        <C 
        catch1={catch1} 
        catch2={catch2} 
        catch3={catch3} 
        catch4={catch4}
        catch5={catch5} 
        catch6={catch6}
        catch7={catch7} 
        catch8={catch8} 
        catch9={catch9} 
        catch10={catch10} 
        info1={info1} 
        info2={info2} 
        info3={info3} 
        info4={info4} 
        info5={info5} 
        info6={info6} 
        info7={info7} 
        info8={info8} 
        info9={info9} 
        info10={info10}
        {...props}/>
    )
}

With the use of this HOC I can have up to ten children in each component. If there is a component that has more than ten then I would need to modify the HOC in order to put the capacity for catching info from more children.

The innermost component

Let's take a look to the definition of the innermost component:

import React,{useEffect,useReducer,useContext} from 'react'
import {reducer,initialState} from './reducer'
import {StoreContext} from '../App'

const Counter=({liftUp,name})=>{
    const names=name.split('-')
    const store=useContext(StoreContext)

    const [state,dispatch]=useReducer(reducer,initialState)

    useEffect(()=>{
        liftUp.bind(null,{state,dispatch})()
    },[state])

    return (
        <div>
            {store[names[0]]&&store[names[0]][names[1]]&&
            store[names[0]][names[1]][names[2]].state.counter}
        </div>
    )
}

export default Counter

As you can see it's a counter component because it defines a state and a dispatch function which are as follows:

import {INCREMENT,DECREMENT} from './actions'

export const initialState={
    counter:0
}

const increment=(state,action)=>{
    return {
        ...state,
        counter:state.counter+1
    }
}

const decrement=(state,action)=>{
    return {
        ...state,
        counter:state.counter-1
    }
}

export const reducer=(state,action)=>{
    switch(action.type){
        case INCREMENT:
            return increment(state,action)
        case DECREMENT:
            return decrement(state,action)
        default:
            return state
    }
}

So you see how we have an initial state with counter set to zero, and then operations to increment and decrement that counter.
The Counter component receives a liftUp property. This is used to lift up info to the parent component of Counter. We do that in a useEffect hook, binding to the liftUp function an object with the info we want to attach, and calling it.


    useEffect(()=>{
        liftUp.bind(null,{state,dispatch})()
    },[state])

The Counters component

Now let's take a look at the definition of the Counters component, the parent of the Counter component, or at least one that have Counter components as a child.

import React,{useReducer,useState,useRef,useEffect,useContext} from 'react'
import Counter from '../Counter'
import * as styles from './index.module.css'
import * as counterActions from '../Counter/actions'
import {reducer,initialState} from './reducer'
import {StoreContext} from '../App'
import withLiftUp from '../../hocs/withLiftUp'

const Counters=({liftUp,name,catch1,catch2,info1,info2})=>{
    const names=name.split('-')
    const store=useContext(StoreContext)
    const [state,dispatch]=useReducer(reducer,initialState)

    const increment1=()=>{
        console.log(store)
        store[names[0]][names[1]][name1].dispatch(counterActions.increment())
    }
    const decrement1=()=>{
        store[names[0]][names[1]][name1].dispatch(counterActions.decrement())
    }

    const increment2=()=>{
        store[names[0]][names[1]][name2].dispatch(counterActions.increment())
    }
    const decrement2=()=>{
        store[names[0]][names[1]][name2].dispatch(counterActions.decrement())
    }

    const name1='counter1'
    const name2='counter2'

    useEffect(()=>{
        liftUp.bind(null,{
            state,dispatch,[name1]:info1.current,[name2]:info2.current
        })()
    },[state,info1.current,info2.current])

    return (
                <div>
            <Counter liftUp={catch1}
            name={name+'-'+name1}/>
            <Counter liftUp={catch2}
            name={name+'-'+name2}/>
            <div>
                <button onClick={increment1}>increment</button><br/>
                <button onClick={decrement1}>decrement</button><br/>
                {store[names[0]]&&store[names[0]][names[1]]&&
                store[names[0]][names[1]][name1]&&store[names[0]][names[1]][name1].state.counter}
            </div>
            <div>
                <button onClick={increment2}>increment</button><br/>
                <button onClick={decrement2}>decrement</button><br/>
                {store[names[0]]&&store[names[0]][names[1]]&&
                store[names[0]][names[1]][name2]&&store[names[0]][names[1]][name2].state.counter}
            </div>
        </div>
    )
}

export default withLiftUp(Counters)

The first thing we notice are the catch1, catch2, info1, and info2 properties we receive:

const Counters=({liftUp,name,catch1,catch2,info1,info2})=>{

That's because we make use of the withLiftUp HOC defined earlier and because we have to children in this component from where we want to get info, that's it:

            <Counter liftUp={catch1}
            name={name+'-'+name1}/>
            <Counter liftUp={catch2}
            name={name+'-'+name2}/>

You see how we pass to the children a property named liftUp with the catch1 and catch2 functions the HOC gives to us.
We then have this:

    const name1='counter1'
    const name2='counter2'

    useEffect(()=>{
        liftUp.bind(null,{
            state,dispatch,[name1]:info1.current,[name2]:info2.current
        })()
    },[state,info1.current,info2.current])

We are passing up the info from the childs. The info from the children will be contained in info1.current and info2.current because info1 and info2 are refs. Take a look at the post mentioned earlier if this is not clear to you.
Don't pay attention now at the names. We are going up through the tree. Later we will go down and will take into account the names.

The Counterss component

This component has as children instances of the Counters component:

import React,{useReducer,useContext,useEffect} from 'react'
import Counters from '../Counters'
import {reducer,initialState} from './reducer'
import withLiftUp from '../../hocs/withLiftUp'
import {StoreContext} from '../App'

const Counterss=({catch1,catch2,info1,info2,name,liftUp})=>{
    const names=name.split('-')
    const store=useContext(StoreContext)
    const [state,dispatch]=useReducer(reducer,initialState)

    const name1='counters1'
    const name2='counters2'

    useEffect(()=>{
        liftUp.bind(null,{state,dispatch,
        [name1]:info1.current,[name2]:info2.current})()
    },[state,dispatch,info1.current,info2.current])

    return (
        <div>
        <Counters liftUp={catch1} name={name+'-'+name1}/>
        <Counters liftUp={catch2} name={name+'-'+name2}/>
        {store[names[0]]&&
        store[names[0]][name1]&&store[names[0]][name1].counter1.state.counter}
        {store[names[0]]&&
        store[names[0]][name1]&&store[names[0]][name1].counter2.state.counter}
        {store[names[0]]&&
        store[names[0]][name2]&&store[names[0]][name2].counter1.state.counter}
        {store[names[0]]&&
        store[names[0]][name2]&&store[names[0]][name2].counter2.state.counter}
        </div>
    )
}

export default withLiftUp(Counterss)

You notice how we receive those props:

const Counterss=({catch1,catch2,info1,info2,name,liftUp})=>{

that's because we have two children:


        <Counters liftUp={catch1} name={name+'-'+name1}/>
        <Counters liftUp={catch2} name={name+'-'+name2}/>

Pay attention also at the naming, we receive a name prop and we define a name prop in each children, where name1 and name2 are defined in the component:


    const name1='counters1'
    const name2='counters2'

We as always pass info up with the use of useEffect hook and liftUp function received as a prop:


    useEffect(()=>{
        liftUp.bind(null,{state,dispatch,
        [name1]:info1.current,[name2]:info2.current})()
    },[state,dispatch,info1.current,info2.current])

The App component

Finally, we get at the top level component, the App component. Here is how it is defined:

import React,{createContext,useState,useEffect,useReducer} from 'react'
import * as classes from './index.module.css'
import Counterss from '../Counterss'
import withLiftUp from '../../hocs/withLiftUp'
import {reducer,initialState} from './reducer'

export const StoreContext=createContext()

const App=({catch1,info1})=>{
    const [store,setStore]=useState({})
    const [state,dispatch]=useReducer(reducer,initialState)

    useEffect(()=>{
        setStore({state,dispatch,[name1]:info1.current})
    },[state,dispatch,info1.current])

    const name1='counterss1'

    return (
    <StoreContext.Provider value={store}>
        <div className={classes.general}>
            <Counterss name={name1} liftUp={catch1}/>
        </div>
    </StoreContext.Provider>
    )
}

export default withLiftUp(App)

First of all we create a context with createContext from react:


export const StoreContext=createContext()

We also create a store object and a setStore function with the useState hook:

const [store,setStore]=useState({})

and we set it like this in the useEffect hook:


    useEffect(()=>{
        setStore({state,dispatch,[name1]:info1.current})
    },[state,dispatch,info1.current])

info1 is received as a prop from the use of the HOC:

const App=({catch1,info1})=>{

We also receive catch1 which is used in here:


            <Counterss name={name1} liftUp={catch1}/>

and name1 is defined as follows:


    const name1='counterss1'

Conclusion

So that's it, this is how to fully take control of state management in React (without Redux).
That's the app running:
Alt Text
Try it yourself with a less complex or cumbersome app.

Discussion (0)

Forem Open with the Forem app