Full State Management in React (without Redux)
roggc
Posted on September 14, 2020
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:
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:
Try it yourself with a less complex or cumbersome app.
Posted on September 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024
November 29, 2024