React: is this a good way to implement a shared state subscription? - javascript

Not sure if this is a so-called "pub/sub" pattern or a form of a "pub/sub" pattern. I am trying to create a piece of shared state so that different components can subscribe to it and only gets updated when there is an update with that state.
const useForceUpdate = () => useReducer((state) => !state, false)[1];
const createSharedState = (reducer, initialState) => {
const subscribers = [];
let state = initialState;
const dispatch = (action) => {
state = reducer(state, action);
subscribers.forEach((callback) => callback());
};
const useSharedState = () => {
const forceUpdate = useForceUpdate();
useEffect(() => {
const callback = () => forceUpdate();
subscribers.push(callback);
const cleanup = () => {
const index = subscribers.indexOf(callback);
subscribers.splice(index, 1);
};
return cleanup;
}, []);
return [state, dispatch];
};
return useSharedState;
};
const initialState = 0;
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
case "set":
return action.count;
default:
return state;
}
};
const useCount1 = createSharedState(reducer, initialState);
const useCount2 = createSharedState(reducer, initialState);
const Counter = ({ count, dispatch }) => (
<div>
{count}
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
<button onClick={() => dispatch({ type: "decrement" })}>-1</button>
<button onClick={() => dispatch({ type: "set", count: 0 })}>reset</button>
</div>
);
const Counter1 = () => {
const [count, dispatch] = useCount1();
return <Counter count={count} dispatch={dispatch} />;
};
const Counter2 = () => {
const [count, dispatch] = useCount2();
return <Counter count={count} dispatch={dispatch} />;
};
const Example = () => (
<>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</>
);
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
<script src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script type="text/babel">
const { useEffect, useReducer } = React;
const useForceUpdate = () => useReducer((state) => !state, false)[1];
const createSharedState = (reducer, initialState) => {
const subscribers = [];
let state = initialState;
const dispatch = (action) => {
state = reducer(state, action);
subscribers.forEach((callback) => callback());
};
const useSharedState = () => {
const forceUpdate = useForceUpdate();
useEffect(() => {
const callback = () => forceUpdate();
subscribers.push(callback);
const cleanup = () => {
const index = subscribers.indexOf(callback);
subscribers.splice(index, 1);
};
return cleanup;
}, []);
return [state, dispatch];
};
return useSharedState;
};
const initialState = 0;
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
case "set":
return action.count;
default:
return state;
}
};
const useCount1 = createSharedState(reducer, initialState);
const useCount2 = createSharedState(reducer, initialState);
const Counter = ({ count, dispatch }) => (
<div>
{count}
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
<button onClick={() => dispatch({ type: "decrement" })}>-1</button>
<button onClick={() => dispatch({ type: "set", count: 0 })}>reset</button>
</div>
);
const Counter1 = () => {
const [count, dispatch] = useCount1();
return <Counter count={count} dispatch={dispatch} />;
};
const Counter2 = () => {
const [count, dispatch] = useCount2();
return <Counter count={count} dispatch={dispatch} />;
};
const Example = () => (
<>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</>
);
ReactDOM.render(<Example />, document.querySelector("#root"));
</script>
It seems to be working fine. My questions are:
Is this a valid way to implement shared update subscription?
Is there any drawbacks with using a simple variable to hold the state + forcing React to re-render if that piece of state changes, instead of using useState or useReducer as one normally would do?
any feedback is welcomed.

Your idea is excellent. React team was also thinking on this topic and ended up with the creation of https://recoiljs.org/. You can use it as useState (DEMO) or as useReducer (DEMO).
I don't want to highlight your solution's drawbacks. Instead, I'd like to list the advantages of using Recoil:
Internal memory usage optimization.
No need to support the code (Facebook does it).
No cheating (useForceUpdate).
Supports selectors out of the box.
I'd recommend you to learn more about Recoil and start using it because it gives the exact result you want to achieve.

Related

dispatch is not a function at onClick in reactjs

I want to write context with UseReducer hook but an error
error this:dispatch is not a function
what is problem?
please help me guys
almost is correct but not working it.
I want to see the appropriate action by clicking on the buttons,
one is increment, one is decrement and the other is reset.
CounterOne
import { UseCount, UseCountActions } from "./CounterProvider";
const CounterOne = () => {
const count = UseCount();
const dispatch = UseCountActions();
return (
<div>
<h2>count is:{count}</h2>
<button onClick={() => dispatch({ type: "add", value: 1 })}>
Addone{" "}
</button>
<button onClick={() => dispatch({ type: "decrement", value: 1 })}>
decrement
</button>
<button onClick={() => dispatch({ type: "reset" })}>reset</button>
</div>
);
};
export default CounterOne;
CounterProvider
import React, { useReducer, useState } from "react";
import { useContext } from "react";
const CounterContext = React.createContext();
const CounterContextDispather = React.createContext();
const initialState = 0;
const reducer = (state, action) => {
switch (action.type) {
case "add":
return state + action.value;
case "decrement":
return state - action.value;
case "reset":
return initialState;
default:
return state;
}
};
const CounterProvider = ({ children }) => {
const [count, dispatch] = useReducer(reducer, initialState);
return (
<CounterContext.Provider value={count}>
<CounterContextDispather.Provider value={dispatch}>
{children}
</CounterContextDispather.Provider>
</CounterContext.Provider>
);
};
export default CounterProvider;
export const UseCount = () => useContext(CounterContext);
export const UseCountActions = () => {
return CounterContextDispather;
};
export const UseCountActions = () => {
return useContext(CounterContextDispather);
};
There is a official example

Why doesn't my component re-render when state changes (redux)?

I'm learning redux and i'm making a sort of pokedex app where i fetch 20 pokemons from pokeapi.co at a time. When the page changes a new list of 20 pokemons is fetched. The problem is that while state changes to the new pokemons, they don't actually render.
App.js
const App = () => {
const dispatch = useDispatch();
const offset = useSelector(state => state.offset);
const limit = useSelector(state => state.limit);
useEffect(() => {
//FETCHES 20 OBJECTS THAT CONTAIN AN URL TO AN INDIVIDUAL POKEMON
dispatch(fetchPokemons(limit, offset));
}, [limit, offset, dispatch]);
...
return (
<div style={{ backgroundColor: '#222222' }}>
<Notification />
<AppBarPokemon />
<Switch>
<Route path="/pokemons">
<PokemonsDisplay CapsFirstLetter={CapsFirstLetter}/>
</Route>
...
PokemonsDisplay.js
const PokemonsDisplay = ({ CapsFirstLetter }) => {
const dispatch = useDispatch();
const classes = useStyles();
const pokemons = useSelector(state => state.pokemons);
console.log(pokemons);
const pageSize = 20;
const totalCount = 898;
const handleClick = (p) => {
dispatch(getOnePokemon(p));
};
return (
<div className={classes.root}>
{pokemons && (
<Grid container spacing={3}>
{pokemons.map(p => (
<Grid item xs={3} key={p.name} className={classes.gridItem} component={Link} onClick={() => handleClick(p)} to={`/pokemons/${p.name}`} data-cy={`pokemon-button-${p.name}`}>
<Paper className={classes.paper && classes.color} elevation={10}>
<p className={classes.p}>#{p.id}</p>
<p className={classes.p}>{CapsFirstLetter(p.name)}</p>
{p.sprites.other["dream_world"]["front_default"] !== null ?
<img className={classes.image} alt={`${p.name}'s sprite`} src={p.sprites.other["dream_world"]["front_default"]}/> :
<img className={classes.image} alt={`${p.name}'s sprite`} src={p.sprites.other["official-artwork"]["front_default"]}/>}
</Paper>
</Grid>
))}
</Grid>
)}
<Pagination
totalCount={totalCount}
pageSize={pageSize}
/>
</div>
);
};
pokemonsReducer.js
import getPokemons from '../services/pokemons';
import axios from 'axios';
import { loadPokemonsFromLS, savePokemonsList } from '../utils/localStoragePokemons';
const pokemonsReducer = (state = [], action) => {
console.log('state is:', state)
switch(action.type){
case 'INIT_POKEMONS':
return action.data;
default:
return state;
};
};
export const fetchPokemons = (limit, offset) => {
return async dispatch => {
try {
const pokemons = loadPokemonsFromLS(limit, offset);
dispatch({
type: 'INIT_POKEMONS',
data: pokemons
});
} catch (error) {
const pokemons = await getPokemons.getPokemons(limit, offset);
let pokemonsArray = [];
let pokemonsObject = {};
pokemons.results.forEach(async (r, i) => {
//FETCHES EACH POKEMON URL AND STORES ITS DATA ON pokemons STATE
const pokemonNow = await axios.get(r.url);
pokemonsArray.push(pokemonNow.data);
pokemonsObject[i] = pokemonNow.data
});
savePokemonsList(limit, offset, pokemonsObject);
dispatch({
type: 'INIT_POKEMONS',
data: pokemonsArray
});
}
};
};
export default pokemonsReducer;
I have tried to dispatch({ data: [...pokemons] })
But it doesnt work.
Also i forgot to add. When i go to my component that is routed to '/' and then back to '/pokemons' they render.
Edit: I think i'm getting there.
i changed the reducer function so that it gets called independently from the dispatch, the problem is that now the action doesn't get fired xD.
export const fetchEverything = async (limit, offset) => {
try {
const pokemons = loadPokemonsFromLS(limit, offset);
initPokemons(pokemons);
} catch (error) {
const pokemonsData = await getPokemons.getPokemons(limit, offset);
let pokemons = [];
let pokemonsObject = {};
console.log(pokemonsData)
pokemonsData.results.forEach(async (r, i) => {
//FETCHES EACH POKEMON URL AND STORES ITS DATA ON pokemons STATE
const pokemonNow = await axios.get(r.url);
pokemonsObject[i] = pokemonNow.data;
//console.log([pokemonNow.data][0]);
pokemons.push(pokemonNow.data);
});
console.log(pokemons);
console.log(pokemonsObject);
savePokemonsList(limit, offset, pokemonsObject);
initPokemons(pokemons);
};
};
export const initPokemons = (pokemons) => {
return dispatch => dispatch({ type: 'INIT_POKEMONS', pokemons: pokemons })
};
const pokemonsReducer = (state = [], action) => {
switch(action.type){
case 'INIT_POKEMONS':
console.log(action);
const newState = action.pokemons
return newState;
default:
return state;
};
};
It happens due to redux state mutation, you can resolve this issue using immer as stated in redux documentation as well. https://www.npmjs.com/package/immer

Multi step form ReactJs with Context API

I am making simple multi step form using only react and context. I tried to use react-form-hook, but as I am a new in react it is a bit difficult for to understand it.
It shows "Error: Maximum update depth exceeded." when I add the user after confirming. Thanks for helping in advance
App.js
function App() {
return (
<GlobalProvider>
<div className="container">
<UserForm />
</div>
</GlobalProvider>
);
}
GlobalState.jsx
import React, { createContext, useReducer } from 'react';
import AppReducer from './AppReducer';
const initialState = {
users: []
}
// Create context
export const GlobalContext = createContext(initialState);
// Provider component
export const GlobalProvider = ({ children }) => {
const [state, dispatch] = useReducer(AppReducer, initialState);
// Actions
function addUser(user) {
dispatch({
type: 'ADD_USERS',
payload: user
});
}
return (<GlobalContext.Provider value={{
users: state.users,
addUser
}}>
{children}
</GlobalContext.Provider>);
}
AppReducer.jsx
export default (state, action) => {
switch(action.type) {
case 'ADD_USERS':
return {
...state,
users: [action.payload, ...state.users]
}
default:
return state;
}
}
UserForm.jsx
const UserForm = () => {
const [step, setStep] = useState(1);
const [user, setUser] = useState({
firstname: '',
lastname: ''
})
const { firstname, lastname } = user;
const {addUser} = useContext(GlobalContext)
// Go Forward to next step
const nextStep = () => {
setStep(step + 1)
};
// Go back to prev step
const prevStep = () => {
setStep(step - 1)
};
const handleChange = input => e => {
setUser({ ...user, [e.target.name]: e.target.value })
}
if (step === 3) {
const newUser = {
id: Math.floor(Math.random() * 100000000),
firstname,
lastname
}
addUser(newUser)
setTimeout(() => {
setStep(1)
clearAll()
}, 1000);
}
const clearAll = () => {
setUser({
firstname: '',
lastname: ''
})
}
switch (step) {
case 1:
return (<UserInfo
prevStep={prevStep}
user={user}
handleChange={handleChange}
/>);
case 2:
return (<Confirm
nextStep={nextStep}
prevStep={prevStep}
user={user}
/>);
case 3:
return <Succuess />;
default:
return null
}
}
export default UserForm
Confirm.jsx
const Confirm = ({nextStep, prevStep, user}) => {
return (
<div>
<h1> Confirm</h1>
<div>
<p>{user.firstname}</p>
<p>{user.lastname}</p>
</div>
<button onClick={(e) => prevStep()} type="button" className="btn btn-primary">Back</button>
<button onClick={(e) => nextStep()} type="button" className="btn btn-success">Submit</button>
</div>
)
}
export default Confirm
I think this code should be inside a function and handled on event:
const checkFinalStep = () = {
if (step === 3) {
const newUser = {
id: Math.floor(Math.random() * 100000000),
firstname,
lastname
}
addUser(newUser)
setTimeout(() => {
setStep(1)
clearAll()
}, 1000);
}
}
Because currently it is simply inside the component, it will run every time react needs to re-render the component. And because it contains addUser reducer, it triggers updating the context which in turn renders the children, re-rendering your component again, recursively.

React useReducer: How to combine multiple reducers?

I'm not a Javascript expert so I wondered if anyone has an "elegant" way to combine multiple reducers to create a global state(Like Redux). A function that does not affect performance when a state updating multiple components etc..
Let's say I have a store.js
import React, { createContext, useReducer } from "react";
import Rootreducer from "./Rootreducer"
export const StoreContext = createContext();
const initialState = {
....
};
export const StoreProvider = props => {
const [state, dispatch] = useReducer(Rootreducer, initialState);
return (
<StoreContext.Provider value={[state, dispatch]}>
{props.children}
<StoreContext.Provider>
);
};
Rootreducer.js
import Reducer1 from "./Reducer1"
import Reducer2 from "./Reducer2"
import Reducer3 from "./Reducer3"
import Reducer4 from "./Reducer4"
const rootReducer = combineReducers({
Reducer1,
Reducer2,
Reducer3,
Reducer4
})
export default rootReducer;
Combine slice reducers (combineReducers)
The most common approach is to let each reducer manage its own property ("slice") of the state:
const combineReducers = (slices) => (state, action) =>
Object.keys(slices).reduce( // use for..in loop, if you prefer it
(acc, prop) => ({
...acc,
[prop]: slices[prop](acc[prop], action),
}),
state
);
Example:
import a from "./Reducer1";
import b from "./Reducer2";
const initialState = { a: {}, b: {} }; // some state for props a, b
const rootReducer = combineReducers({ a, b });
const StoreProvider = ({ children }) => {
const [state, dispatch] = useReducer(rootReducer, initialState);
// Important(!): memoize array value. Else all context consumers update on *every* render
const store = React.useMemo(() => [state, dispatch], [state]);
return (
<StoreContext.Provider value={store}> {children} </StoreContext.Provider>
);
};
Combine reducers in sequence
Apply multiple reducers in sequence on state with arbitrary shape, akin to reduce-reducers:
const reduceReducers = (...reducers) => (state, action) =>
reducers.reduce((acc, nextReducer) => nextReducer(acc, action), state);
Example:
const rootReducer2 = reduceReducers(a, b);
// rest like in first variant
Combine multiple useReducer Hooks
You could also combine dispatch and/or state from multiple useReducers, like:
const combineDispatch = (...dispatches) => (action) =>
dispatches.forEach((dispatch) => dispatch(action));
Example:
const [s1, d1] = useReducer(a, {}); // some init state {}
const [s2, d2] = useReducer(b, {}); // some init state {}
// don't forget to memoize again
const combinedDispatch = React.useCallback(combineDispatch(d1, d2), [d1, d2]);
const combinedState = React.useMemo(() => ({ s1, s2, }), [s1, s2]);
// This example uses separate dispatch and state contexts for better render performance
<DispatchContext.Provider value={combinedDispatch}>
<StateContext.Provider value={combinedState}> {children} </StateContext.Provider>
</DispatchContext.Provider>;
In summary
Above are the most common variants. There are also libraries like use-combined-reducers for these cases. Last, take a look at following sample combining both combineReducers and reduceReducers:
const StoreContext = React.createContext();
const initialState = { a: 1, b: 1 };
// omit distinct action types for brevity
const plusOneReducer = (state, _action) => state + 1;
const timesTwoReducer = (state, _action) => state * 2;
const rootReducer = combineReducers({
a: reduceReducers(plusOneReducer, plusOneReducer), // aNew = aOld + 1 + 1
b: reduceReducers(timesTwoReducer, plusOneReducer) // bNew = bOld * 2 + 1
});
const StoreProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(rootReducer, initialState);
const store = React.useMemo(() => [state, dispatch], [state]);
return (
<StoreContext.Provider value={store}> {children} </StoreContext.Provider>
);
};
const Comp = () => {
const [globalState, globalDispatch] = React.useContext(StoreContext);
return (
<div>
<p>
a: {globalState.a}, b: {globalState.b}
</p>
<button onClick={globalDispatch}>Click me</button>
</div>
);
};
const App = () => <StoreProvider> <Comp /> </StoreProvider>
ReactDOM.render(<App />, document.getElementById("root"));
//
// helpers
//
function combineReducers(slices) {
return (state, action) =>
Object.keys(slices).reduce(
(acc, prop) => ({
...acc,
[prop]: slices[prop](acc[prop], action)
}),
state
)
}
function reduceReducers(...reducers){
return (state, action) =>
reducers.reduce((acc, nextReducer) => nextReducer(acc, action), state)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
If you simply want to achieve a combine reducer feature without any third-party library, do it as below. (REF: Redux source/code)
The working code is here https://codepen.io/rajeshpillai/pen/jOPWYzL?editors=0010
I have two reducers created, one dateReducer and another counterReducer. I am using it as
const [state, dispatch] = useReducer(combineReducers({
counter: counterReducer,
date: dateReducer
}), initialState);
The combineReducers code
function combineReducers(reducers) {
return (state = {}, action) => {
const newState = {};
for (let key in reducers) {
newState[key] = reducers[key](state[key], action);
}
return newState;
}
}
Usage: Extract the respective state
const { counter, date } = state;
NOTE: You can add more redux like features if you wish.
The complete working code (in case codepen is down :))
const {useReducer, useEffect} = React;
function dateReducer(state, action) {
switch(action.type) {
case "set_date":
return action.payload;
break;
default:
return state;
}
}
function counterReducer(state, action) {
console.log('cr:', state);
switch (action.type) {
case 'increment': {
return state + 1;
}
case 'decrement': {
return state - 1;
}
default:
return state;
}
}
function combineReducers(reducers) {
return (state = {}, action) => {
const newState = {};
for (let key in reducers) {
newState[key] = reducers[key](state[key], action);
}
return newState;
}
}
const initialState = {
counter: 0,
date: new Date
};
function App() {
const [state, dispatch] = useReducer(combineReducers({
counter: counterReducer,
date: dateReducer
}), initialState);
console.log("state", state);
const { counter, date } = state;
return (
<div className="app">
<h3>Counter Reducer</h3>
<div className="counter">
<button onClick={() =>
dispatch({ type: 'increment'})}>+
</button>
<h2>{counter.toString()}</h2>
<button onClick={() =>
dispatch({ type: 'decrement'})}>-
</button>
</div>
<hr/>
<h3>Date Reducer</h3>
{date.toString()}
<button className="submit"
type="submit"
onClick={() =>
dispatch({ type: 'set_date', payload:new Date })}>
Set Date
</button>
</div>
);
}
const rootElement = document.querySelector("#root");
ReactDOM.render(<App />, rootElement);
NOTE: This is a quick hack (for learning and demonstration purpose only)
There is a library called react combine reducer that is specifically use for combining reducer with the context api. Below is the code sample
import { useReducer } from 'react';
import combineReducers from 'react-combine-reducers';
const initialIdentity = {
name: 'Harry'
}
const initialLocation = {
country: 'UK',
city: 'London'
}
const identityReducer = (state, action) => {
switch (action.type) {
case 'ACTION_A':
return { ...state, name: 'Puli' };
default: return state;
}
}
const locationReducer = (state, action) => {
switch (action.type) {
case 'ACTION_B':
return { ...state, city: 'Manchester' };
default: return state;
}
}
const [profileReducer, initialProfile] = combineReducers({
identity: [identityReducer, initialIdentity],
location: [locationReducer, initialLocation]
});
const [state, dispatch] = useReducer(profileReducer, initialProfile);
console.log(state);
// Outputs the following state:
// {
// identity: {
// name: "Harry"
// },
// location: {
// country: "UK",
// city: "London"
// }
// }
In your rootReducer.js file you can use combineReducers from redux to combine multiple reducers. The traditional way is:
import { combineReducers } from 'redux';
const rootReducer = combineReducers({ name: nameReducer});
export default rootReducer;
You can import the rootReducer while creating the store as:
import { combineReducers } from 'redux';
let store = createStore(rootReducer);
While using useReducer hook you can pass the rootReducer to it:
const [state, dispatch] = useReducer(rootReducer, initialState);
Hope this works for you.
Instead of using useReducer use useCombineReducers() . may change this function to accept multiple parameters based on your requirement
const inti ={ count:0, alpha:''}
export function reducer1(state, action) {
switch (action.type)
{
case 'increment':
return {...state , count: state.count + 1};
case 'decrement':
return {...state , count: state.count - 1};
default:
return {count:0};
} }
export function reducer2(state, action) {
switch (action.type) {
case 'add':
return {...state , alpha: state.alpha + action.payload };
case 'rem':
return {...state , alpha: state.alpha + action.payload};
default:
return {alpha:''};
}}
function useCombineReducers(reducer1,reducer2, init) {
const [state,setState] = useState(init);
function dispatch(action)
{
let ns = null;
if(action.type == 'add' || action.type=="rem")
{
ns = reducer2(state,action)
}
else
{
ns = reducer1(state,action)
}
setState(ns);
}
return [state, dispatch];}
function App() {
const [state,dispatch] = useCombineReducers(reducer1,reducer2,inti);
return (
<>
<Provider >
<Counter state ={state} dispatch={dispatch}></Counter>
<Alpha state ={state} dispatch={dispatch}></Alpha>
</Provider>
</>
); }
const Counter = (props) => {
return (
<div style ={{Border:'10px', width:'20px'}}>
Count : {props.state.count}
<button onClick={()=> props.dispatch({type: 'increment'})}> + </button>
<button onClick={()=> props.dispatch({type: 'decrement'})}> - </button>
</div>
)} export default Counter
const Alpha = (props) => {
return (
<div style ={{Border:'10px', width:'20px'}}>
Alpha : {props.state.alpha}
<button onClick={()=> props.dispatch({type: 'add',payload:'+'})}> + </button>
<button onClick={()=> props.dispatch({type: 'rem',payload:'-'})}> - </button>
</div>
)} export default Alpha
I played around a bit and were thinking about the problem as I had to handle it as well.
This might not be the best approach but I just defined my reducers as objects with key: reducer function combinations:
const counterRed = {
increment: (oldState, action) => ({
...oldState,
counter: oldState.counter + 1
}),
decrement: (oldState, action) => ({
...oldState,
counter: oldState.counter - 1
})
};
and
const dateRed = {
set_date: (oldState, action) => ({ ...oldState, date: action.payload })
};
and I combined them like this:
const reducer = (oldState, action) => {
const combinedReducers = { ...dateRed, ...counterRed };
let newState = null;
if (combinedReducers[action.type]) {
newState = combinedReducers[action.type](oldState, action);
}
if (newState) {
return { ...newState };
}
return oldState;
};
a working example can be seen here: https://codesandbox.io/s/jovial-kowalevski-25pzf?file=/src/App.js

How to access child's state in React? (React Hooks)

What's the best solution to get access to the child's state using react-hooks?
I've tried various approaches. Below the one that I've ended up with.
Form (Parent)
export const Form: FunctionComponent<IProps> = ({ onFinish, initState }) => {
const formInputsStateRef = useRef({})
const handleFinish = () => {
const params = formInputsStateRef.current
console.log(params)
onFinish(params)
}
return (
<div>
<Inputs initState={initState} stateRef={formInputsStateRef} />
<S.Button onClick={handleFinish}>
Finish
</S.Button>
</div>
)
}
Inputs (Child)
export const Inputs: FunctionComponent<IProps> = ({ initState, stateRef }) => {
const [pool, setPool] = useState(initState.pool)
const [solarPanel, setSolarPanel] = useState(initState.solarPanel)
useEffect(() => {
stateRef.current = { pool, solarPanel }
})
const handlePoolInput = () => {
setPool('new pool')
}
const handleSolarPanelInput = () => {
setSolarPanel('new solar panel')
}
return (
<div>
<h2>{pool}</h2>
<S.Button onClick={handlePoolInput}>Change pool</S.Button>
<h2>{solarPanel}</h2>
<S.Button onClick={handleSolarPanelInput}>Change solar panel</S.Button>
<h2>-----</h2>
</div>
)
}
It works that way but I don't like the fact that it creates an object on every render.
Inputs(Child)
useEffect(() => {
stateRef.current = { pool, solarPanel }
})
You could pass pool and solarPanel as the second argument to useEffect so that the state is updated to ref only on these values change
export const Inputs: FunctionComponent<IProps> = ({ initState, stateRef }) => {
const [pool, setPool] = useState(initState.pool)
const [solarPanel, setSolarPanel] = useState(initState.solarPanel)
useEffect(() => {
stateRef.current = { pool, solarPanel }
}, [pool, solarPanel])
const handlePoolInput = () => {
setPool('new pool')
}
const handleSolarPanelInput = () => {
setSolarPanel('new solar panel')
}
return (
<div>
<h2>{pool}</h2>
<S.Button onClick={handlePoolInput}>Change pool</S.Button>
<h2>{solarPanel}</h2>
<S.Button onClick={handleSolarPanelInput}>Change solar panel</S.Button>
<h2>-----</h2>
</div>
)
}
However to have a more controlled handle of child values using ref, you can make use of useImperativeHandle hook.
Child
const InputsChild: FunctionComponent<IProps> = ({ initState, ref }) => {
const [pool, setPool] = useState(initState.pool)
const [solarPanel, setSolarPanel] = useState(initState.solarPanel)
useImperativeHandle(ref, () => ({
pool,
solarPanel
}), [pool, solarPanel])
const handlePoolInput = () => {
setPool('new pool')
}
const handleSolarPanelInput = () => {
setSolarPanel('new solar panel')
}
return (
<div>
<h2>{pool}</h2>
<S.Button onClick={handlePoolInput}>Change pool</S.Button>
<h2>{solarPanel}</h2>
<S.Button onClick={handleSolarPanelInput}>Change solar panel</S.Button>
<h2>-----</h2>
</div>
)
}
export const Inputs = forwardRef(InputsChild);
Parent
export const Form: FunctionComponent<IProps> = ({ onFinish, initState }) => {
const formInputsStateRef = useRef({})
const handleFinish = () => {
const params = formInputsStateRef.current
console.log(params)
onFinish(params)
}
return (
<div>
<Inputs initState={initState} ref={formInputsStateRef} />
<S.Button onClick={handleFinish}>
Finish
</S.Button>
</div>
)
}

Categories

Resources