According to React docs :
useReducer is usually preferable to useState when you have complex
state logic that involves multiple sub-values or when the next state
depends on the previous one.
1. can somebody explain me why useReducer is not updating the state synchronously ?
const reducer = (state, action) => {
if( action.type === 'ADD_VALUE') {
console.log(`STATE IN REDUCER`, [...state, action.path]) // => ["1.1"]
return [...state, action.path]
}
}
const [state, dispatch] = useReducer(reducer, [])
<input type="button" onClick={() => {
dispatch({ type: 'ADD_VALUE', path: "1.1"})
console.log(`STATE`, state) // => []
// here i want to do some stuff based on the lastest updated state (["1.1"] and not [])
// for example dispatch an action with redux
}}/>
2. How can I do some stuff (dispatch a redux action) based on the lastest updated state (["1.1"] and not []) ?
Use useEffect to access the state correctly. You could add some safe-guarding if you want something invoking if a certain criterion is hit.
If you want to access your reducer across components, you can store the reducer using Context API. Look below for an example. You can see the reducer being injected into the Context on the parent component and then two child components that a) dispatches an action b) receives the update from the action.
1. Example of context reducer to use across multiple components
import React from "react";
import ReactDOM from "react-dom";
const Application = React.createContext({
state: null,
dispatch: null
});
function ActionComponent() {
const { dispatch } = React.useContext(Application);
return (
<div>
<div>Action Component</div>
<button onClick={() => dispatch("lol")}>Do something</button>
</div>
);
}
function ListenerComponent() {
const { state } = React.useContext(Application);
React.useEffect(
() => {
console.log(state);
},
[state]
);
return <div>Listener Component</div>;
}
function App() {
const [state, dispatch] = React.useReducer(function(state = [], action) {
return [...state, action];
});
return (
<Application.Provider value={{ state, dispatch }}>
<div className="App">
<ActionComponent />
<ListenerComponent />
</div>
</Application.Provider>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
2. Example of local reducer without using Application Context
const reducer = (state, action) => {
if( action.type === 'ADD_VALUE') {
return [...state, action.path]
}
}
const [state, dispatch] = useReducer(reducer, [])
React.useEffect(() => {
console.log(state);
}, [state]);
<input type="button" onClick={() => {
dispatch({ type: 'ADD_VALUE', path: "1.1"})
}}/>
Related
I have a question, if I can use useState generic in React Hooks, just like I can do this in React Components while managing multiple states?
state = {
input1: "",
input2: "",
input3: ""
// .. more states
};
handleChange = (event) => {
const { name, value } = event.target;
this.setState({
[name]: value,
});
};
Yes, with hooks you can manage complex state (without 3rd party library) in three ways, where the main reasoning is managing state ids and their corresponding elements.
Manage a single object with multiple states (notice that an array is an object).
Use useReducer if (1) is too complex.
Use multiple useState for every key-value pair (consider the readability and maintenance of it).
Check out this:
// Ids-values pairs.
const complexStateInitial = {
input1: "",
input2: "",
input3: ""
// .. more states
};
function reducer(state, action) {
return { ...state, [action.type]: action.value };
}
export default function App() {
const [fromUseState, setState] = useState(complexStateInitial);
// handle generic state from useState
const onChangeUseState = (e) => {
const { name, value } = e.target;
setState((prevState) => ({ ...prevState, [name]: value }));
};
const [fromReducer, dispatch] = useReducer(reducer, complexStateInitial);
// handle generic state from useReducer
const onChangeUseReducer = (e) => {
const { name, value } = e.target;
dispatch({ type: name, value });
};
return (
<>
<h3>useState</h3>
<div>
{Object.entries(fromUseState).map(([key, value]) => (
<input
key={key}
name={key}
value={value}
onChange={onChangeUseState}
/>
))}
<pre>{JSON.stringify(fromUseState, null, 2)}</pre>
</div>
<h3>useReducer</h3>
<div>
{Object.entries(fromReducer).map(([key, value]) => (
<input
name={key}
key={key}
value={value}
onChange={onChangeUseReducer}
/>
))}
<pre>{JSON.stringify(fromReducer, null, 2)}</pre>
</div>
</>
);
}
Notes
Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
Refer to React Docs.
The correct way to do what you're trying to do is to create your own hook that uses useState internally.
Here is an example:
// This is your generic reusable hook.
const useHandleChange = (initial) => {
const [value, setValue] = React.useState(initial);
const handleChange = React.useCallback(
(event) => setValue(event.target.value), // This is the meaty part.
[]
);
return [value, handleChange];
}
const App = () => {
// Here we use the hook 3 times to show it's reusable.
const [value1, handle1] = useHandleChange('one');
const [value2, handle2] = useHandleChange('two');
const [value3, handle3] = useHandleChange('three');
return <div>
<div>
<input onChange={handle1} value={value1} />
<input onChange={handle2} value={value2} />
<input onChange={handle3} value={value3} />
</div>
<h2>States:</h2>
<ul>
<li>{value1}</li>
<li>{value2}</li>
<li>{value3}</li>
</ul>
</div>
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Note the use of React.useCallback to stop your hook from returning a new handler function on every render. (We don't need to specify setValue as a dependency because React guarantees that it will never change)
I didn't actually test this, but it should work.
See https://reactjs.org/docs/hooks-reference.html#usestate for more info.
import React, {useState} from 'react';
const MyComponent = () => {
const [name, setName] = useState('Default value for name');
return (<div><button onClick={()=>setName('John Doe')}}>Set Name</button></div>);
};
export default MyComponent;
I am refactoring the complex state management of one of my contexts, which previously used the hook "useStateWithCallback".
I have decided to refactor the code using "useReducer", which will make it more readable.
Currently, I am doing this:
export function ContentsProvider({ children }) {
const [contents, setContents] = useStateWithCallback(new Map([]));
const addContents = (newContents, callback = undefined) => {
setContents(
(prevContents) => new Map([...prevContents, ...newContents]),
callback
);
};
...
Now, on any of my app components which consumes this context I can do:
contents.addContents([ ... ], () => { ... });
And the callback will be executed only when the state has changed.
Is there any way to achieve this behavior with useReducer? I mean, if I am passing the callback as argument to the method of my provider, then I will be not able to run a
useEffect(() => { callback?.() } , [contents]);
Any ideas?
I have tried this:
import { useReducer, useEffect } from 'react'
export const useReducerWithCallback = (
initialState,
reducer,
callback = undefined
) => {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
callback?.(state);
}, [state, callback]);
return [state, dispatch];
};
But it will not work, as in my use case, the callback is not always the same, so I mustn't declare the reducer with the callback.
If I understand what you are looking for then I think a custom hook using a React ref and effect might achieve the behavior you seek.
Use a React ref to hold a reference to a callback
Use useEffect to call the callback when the state has updated
Return a custom dispatch function to set the callback and dispatch an action to the reducer function.
Code
const useReducerWithCallback = (reducer, initialState, initializer) => {
const callbackRef = useRef();
const [state, dispatch] = useReducer(reducer, initialState, initializer);
useEffect(() => {
callbackRef.current?.(state);
}, [state]);
const customDispatch = (action, callback) => {
callbackRef.current = callback;
dispatch(action);
};
return [state, customDispatch];
};
const useReducerWithCallback = (reducer, initialState, initializer) => {
const callbackRef = React.useRef();
const [state, dispatch] = React.useReducer(reducer, initialState, initializer);
React.useEffect(() => {
callbackRef.current && callbackRef.current(state);
}, [state]);
const customDispatch = (action, callback) => {
callbackRef.current = callback;
dispatch(action);
};
return [state, customDispatch];
};
const reducer = (state, action) => {
switch (action.type) {
case "ADD":
return state + action.payload;
case "SUBTRACT":
return state - action.payload;
default:
return state;
}
};
function App() {
const [state, dispatch] = useReducerWithCallback(reducer, 0);
return (
<div className="App">
State: {state}
<button
type="button"
onClick={() =>
dispatch({ type: "ADD", payload: 1 }, (state) =>
console.log("Added 1, state", state)
)
}
>
+
</button>
<button
type="button"
onClick={() =>
dispatch({ type: "SUBTRACT", payload: 1 }, (state) =>
console.log("Subtracted 1, state", state)
)
}
>
-
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Drew's answer is great! If you're a TypeScript user, you could potentially type this function using the types from the #types/react declaration file:
/**
* Defines the custom dispatch function that wraps the dispatch returned from useReducer.
*/
type CustomReactDispatchWithCallback<A, S> = (action: A, callback?: DispatchCallback<S>) => void;
/**
* Defines the callback contract. It should be a function that receives the updated state.
*/
type DispatchCallback<S> = (state: S) => void;
/**
* Wraps `React.useReducer` and provides a custom dispatch function that accepts
* a callback that will be cached and then invoked when the reducer state changes.
*/
export function useReducerWithCallback<R extends React.Reducer<any, any>, I>(
reducer: R,
initialState: I & React.ReducerState<R>,
initializer: (arg: I & React.ReducerState<R>) => React.ReducerState<R>
) {
const callbackRef = React.useRef<DispatchCallback<React.ReducerState<R>>>();
const [state, dispatch] = React.useReducer(reducer, initialState, initializer);
React.useEffect(() => {
callbackRef.current?.(state);
}, [state]);
const customDispatch: CustomReactDispatchWithCallback<
React.ReducerAction<R>,
React.ReducerState<R>
> = (action, callback) => {
callbackRef.current = callback;
dispatch(action);
};
return [state, customDispatch] as const;
}
This doesn't account for all of React.useReducer's overloads, but worked for my case.
I can't quite figure out the optimal way to use useReducer hook for data management. My primary goal is to reduce (heh) the boilerplate to minimum and maintain code readability, while using the optimal approach in terms of performance and preventing unnecessary re-renders.
The setup
I have created a simplified example of my app, basically it's a <List /> component - a list of items with possibility to select them, and a <Controls /> component which can switch item groups and reload the data set.
List.js
import React, { memo } from "react";
const List = ({ items, selected, selectItem, deselectItem }) => {
console.log("<List /> render");
return (
<ul className="List">
{items.map(({ id, name }) => (
<li key={`item-${name.toLowerCase()}`}>
<label>
<input
type="checkbox"
checked={selected.includes(id)}
onChange={(e) =>
e.target.checked ? selectItem(id) : deselectItem(id)
}
/>
{name}
</label>
</li>
))}
</ul>
);
};
export default memo(List);
Controls.js
import React, { memo } from "react";
import { ItemGroups } from "./constants";
const Controls = ({ group, setGroup, fetchItems }) => {
console.log("<Controls /> render");
return (
<div className="Controls">
<label>
Select group
<select value={group} onChange={(e) => setGroup(e.target.value)}>
<option value={ItemGroups.PEOPLE}>{ItemGroups.PEOPLE}</option>
<option value={ItemGroups.TREES}>{ItemGroups.TREES}</option>
</select>
</label>
<button onClick={() => fetchItems(group)}>Reload data</button>
</div>
);
};
export default memo(Controls);
App.js
import React, { useEffect, useReducer } from "react";
import Controls from "./Controls";
import List from "./List";
import Loader from "./Loader";
import { ItemGroups } from "./constants";
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import fetchItemsFromAPI from "./api";
import "./styles.css";
const itemsReducer = (state, action) => {
const { type, payload } = action;
console.log(`reducer action "${type}" dispatched`);
switch (type) {
case FETCH_START:
return {
...state,
isLoading: true
};
case FETCH_SUCCESS:
return {
...state,
items: payload.items,
isLoading: false
};
case SET_GROUP:
return {
...state,
selected: state.selected.length ? [] : state.selected,
group: payload.group
};
case SELECT_ITEM:
return {
...state,
selected: [...state.selected, payload.id]
};
case DESELECT_ITEM:
return {
...state,
selected: state.selected.filter((id) => id !== payload.id)
};
default:
throw new Error("Unknown action type in items reducer");
}
};
export default function App() {
const [state, dispatch] = useReducer(itemsReducer, {
items: [],
selected: [],
group: ItemGroups.PEOPLE,
isLoading: false
});
const { items, group, selected, isLoading } = state;
const fetchItems = (group) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
const setGroup = (group) => {
dispatch({
type: SET_GROUP,
payload: { group }
});
};
const selectItem = (id) => {
dispatch({
type: SELECT_ITEM,
payload: { id }
});
};
const deselectItem = (id) => {
dispatch({
type: DESELECT_ITEM,
payload: { id }
});
};
useEffect(() => {
console.log("use effect on group change");
fetchItems(group);
}, [group]);
console.log("<App /> render");
return (
<div className="App">
<Controls {...{ group, fetchItems, setGroup }} />
{isLoading ? (
<Loader />
) : (
<List {...{ items, selected, selectItem, deselectItem }} />
)}
</div>
);
}
Here's the complete sandbox.
The state is managed in a reducer, because I need different parts of state to work and change together. For example, reset selected items on group change (because it makes no sense to keep selections between different data sets), set loaded items and clear loading state on data fetch success, etc. The example is intentionally simple, but in reality there're many dependencies between different parts of state (filtering, pagination, etc.), which makes reducer a perfect tool to manage it - in my opinion.
I've created helper functions to perform different actions (for ex., to reload items or to select/deselect). I could just pass down the dispatch to children and create action objects there, but this turns everything into a mess really quickly, esp. when multiple components must perform same actions.
Problem 1
Passing down reducer action functions to child components causes them to re-render on any reducer update.
Case 1: When I select an item in <List />, the <Controls /> is
re-rendered.
Case 2: When I reload the data on Reload button click, the <Controls /> is
re-rendered.
In both cases, the <Controls /> only actually depends on group prop to render, so when it stays the same - the component should not re-render.
I've investigated it and this happens because on each <App /> re-render these action functions are re-created and treated as new prop values for child components, so for React it's simple: new props => new render.
Not ideal solution to this is to wrap all action functions in useCallback, with dispatch as a dependency, but this looks like a hack to me.
const setGroup = useCallback(
(group) => {
dispatch({
type: SET_GROUP,
payload: { group }
});
},
[dispatch]
);
In a simple example it does not look too bad, but when you have dozens of possible actions, all wrapped in useCallback, with deps arrays - that does not seem right.
And it requires to add even more deps to useEffect (which is another problem).
Here's a "fixed" version with useCallback.
Problem 2
I cannot fully extract reducer action functions outside the <App /> component, because in the end they must be used inside a React component with the dispatch (because it's a hook).
I can of course extract them to a separate module and pass dispatch as a first argument:
in actions.js
// ...
export const fetchItems = (dispatch, group) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
// ...
and then in child components do this:
import { fetchItems } from './actions';
const Child = ({ dispatch, group }) => {
fetchItems(dispatch, group);
// ...
};
and reduce my <App /> to this:
// ...
const App = () => {
const [{ items, group, selected, isLoading }, dispatch] = useReducer(
itemsReducer,
itemReducerDefaults
);
useEffect(() => {
fetchItems(dispatch, group);
}, [group, dispatch]);
return (
<div className="App">
<Controls {...{ group, dispatch }} />
{isLoading ? <Loader /> : <List {...{ items, selected, dispatch }} />}
</div>
);
};
but then I have to pass around the dispatch (minor issue) and always have it in arguments list. On the other hand, it fixes the Problem 1 as well, as dispatch does not change between renders.
Here's a sandbox with actions and reducer extracted.
But is it optimal, or maybe I should use some entirely different approach?
So, how do you guys use it? The React docs and guides are nice and clean with counter increments and ToDo lists, but how do you actually use it in real world apps?
React-redux works by also wrapping all the actions with a call to dispatch; this is abstracted away when using the connect HOC, but still required when using the useDispatch hook. Async actions typically have a function signature (...args) => dispatch => {} where the action creator instead returns a function that accepts the dispatch function provided by redux, but redux requires middleware to handle these. Since you are not actually using Redux you'd need to handle this yourself, likely using a combination of both patterns to achieve similar usage.
I suggest the following changes:
De-couple and isolate your action creators, they should be functions that return action objects (or asynchronous action functions).
Create a custom dispatch function that handles asynchronous actions.
Correctly log when a component renders (i.e. during the commit phase in an useEffect hook and not during any render phase in the component body. See this lifecycle diagram.
Pass the custom dispatch function to children, import actions in children... dispatch actions in children. How to avoid passing callbacks down.
Only conditionally render the Loader component. When you render one or the other of Loader and List the other is unmounted.
Actions (actions.js)
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import fetchItemsFromAPI from "./api";
export const setGroup = (group) => ({
type: SET_GROUP,
payload: { group }
});
export const selectItem = (id) => ({
type: SELECT_ITEM,
payload: { id }
});
export const deselectItem = (id) => ({
type: DESELECT_ITEM,
payload: { id }
});
export const fetchItems = (group) => (dispatch) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
useAsyncReducer.js
const asyncDispatch = (dispatch) => (action) =>
action instanceof Function ? action(dispatch) : dispatch(action);
export default (reducer, initialArg, init) => {
const [state, syncDispatch] = React.useReducer(reducer, initialArg, init);
const dispatch = React.useMemo(() => asyncDispatch(syncDispatch), []);
return [state, dispatch];
};
Why doesn't useMemo need a dependency on useReducer dispatch function?
useReducer
Note
React guarantees that dispatch function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
We want to also provide a stable dispatch function reference.
App.js
import React, { useEffect } from "react";
import useReducer from "./useAsyncReducer";
import Controls from "./Controls";
import List from "./List";
import Loader from "./Loader";
import { ItemGroups } from "./constants";
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import { fetchItems } from "./actions";
export default function App() {
const [state, dispatch] = useReducer(itemsReducer, {
items: [],
selected: [],
group: ItemGroups.PEOPLE,
isLoading: false
});
const { items, group, selected, isLoading } = state;
useEffect(() => {
console.log("use effect on group change");
dispatch(fetchItems(group));
}, [group]);
React.useEffect(() => {
console.log("<App /> render");
});
return (
<div className="App">
<Controls {...{ group, dispatch }} />
{isLoading && <Loader />}
<List {...{ items, selected, dispatch }} />
</div>
);
}
Controls.js
import React, { memo } from "react";
import { ItemGroups } from "./constants";
import { setGroup, fetchItems } from "./actions";
const Controls = ({ dispatch, group }) => {
React.useEffect(() => {
console.log("<Controls /> render");
});
return (
<div className="Controls">
<label>
Select group
<select
value={group}
onChange={(e) => dispatch(setGroup(e.target.value))}
>
<option value={ItemGroups.PEOPLE}>{ItemGroups.PEOPLE}</option>
<option value={ItemGroups.TREES}>{ItemGroups.TREES}</option>
</select>
</label>
<button onClick={() => dispatch(fetchItems(group))}>Reload data</button>
</div>
);
};
List.js
import React, { memo } from "react";
import { deselectItem, selectItem } from "./actions";
const List = ({ dispatch, items, selected }) => {
React.useEffect(() => {
console.log("<List /> render");
});
return (
<ul className="List">
{items.map(({ id, name }) => (
<li key={`item-${name.toLowerCase()}`}>
<label>
<input
type="checkbox"
checked={selected.includes(id)}
onChange={(e) =>
dispatch((e.target.checked ? selectItem : deselectItem)(id))
}
/>
{name}
</label>
</li>
))}
</ul>
);
};
Loader.js
const Loader = () => {
React.useEffect(() => {
console.log("<Loader /> render");
});
return <div>Loading data...</div>;
};
Let's say I'm having a Parent Component providing a Context which is a Store Object. For simplicity lets say this Store has a value and a function to update this value
class Store {
// value
// function updateValue() {}
}
const Parent = () => {
const [rerender, setRerender] = useState(false);
const ctx = new Store();
return (
<SomeContext.Provider value={ctx}>
<Children1 />
<Children2 />
.... // and alot of component here
</SomeContext.Provider>
);
};
const Children1 = () => {
const ctx = useContext(SomeContext);
return (<div>{ctx.value}</div>)
}
const Children2 = () => {
const ctx = useContext(SomeContext);
const onClickBtn = () => {ctx.updateValue('update')}
return (<button onClick={onClickBtn}>Update Value </button>)
}
So basically Children1 will display the value, and in Children2 component, there is a button to update the value.
So my problem right now is when Children2 updates the Store value, Children1 is not rerendered. to reflect the new value.
One solution on stack overflow is here. The idea is to create a state in Parent and use it to pass the context to childrens. This will help to rerender Children1 because Parent is rerendered.
However, I dont want Parent to rerender because in Parent there is a lot of other components. I only want Children1 to rerender.
So is there any solution on how to solve this ? Should I use RxJS to do reative programming or should I change something in the code? Thanks
You can use context like redux lib, like below
This easy to use and later if you want to move to redux you change only the store file and the entire state management thing will be moved to redux or any other lib.
Running example:
https://stackblitz.com/edit/reactjs-usecontext-usereducer-state-management
Article: https://rsharma0011.medium.com/state-management-with-react-hooks-and-context-api-2968a5cf5c83
Reducers.js
import { combineReducers } from "./Store";
const countReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
default:
return state;
}
};
export default combineReducers({ countReducer });
Store.js
import React, { useReducer, createContext, useContext } from "react";
const initialState = {};
const Context = createContext(initialState);
const Provider = ({ children, reducers, ...rest }) => {
const defaultState = reducers(undefined, initialState);
if (defaultState === undefined) {
throw new Error("reducer's should not return undefined");
}
const [state, dispatch] = useReducer(reducers, defaultState);
return (
<Context.Provider value={{ state, dispatch }}>{children}</Context.Provider>
);
};
const combineReducers = reducers => {
const entries = Object.entries(reducers);
return (state = {}, action) => {
return entries.reduce((_state, [key, reducer]) => {
_state[key] = reducer(state[key], action);
return _state;
}, {});
};
};
const Connect = (mapStateToProps, mapDispatchToProps) => {
return WrappedComponent => {
return props => {
const { state, dispatch } = useContext(Context);
let localState = { ...state };
if (mapStateToProps) {
localState = mapStateToProps(state);
}
if (mapDispatchToProps) {
localState = { ...localState, ...mapDispatchToProps(dispatch, state) };
}
return (
<WrappedComponent
{...props}
{...localState}
state={state}
dispatch={dispatch}
/>
);
};
};
};
export { Context, Provider, Connect, combineReducers };
App.js
import React from "react";
import ContextStateManagement from "./ContextStateManagement";
import CounterUseReducer from "./CounterUseReducer";
import reducers from "./Reducers";
import { Provider } from "./Store";
import "./style.css";
export default function App() {
return (
<Provider reducers={reducers}>
<ContextStateManagement />
</Provider>
);
}
Component.js
import React from "react";
import { Connect } from "./Store";
const ContextStateManagement = props => {
return (
<>
<h3>Global Context: {props.count} </h3>
<button onClick={props.increment}>Global Increment</button>
<br />
<br />
<button onClick={props.decrement}>Global Decrement</button>
</>
);
};
const mapStateToProps = ({ countReducer }) => {
return {
count: countReducer.count
};
};
const mapDispatchToProps = dispatch => {
return {
increment: () => dispatch({ type: "INCREMENT" }),
decrement: () => dispatch({ type: "DECREMENT" })
};
};
export default Connect(mapStateToProps, mapDispatchToProps)(
ContextStateManagement
);
If you don't want your Parent component to re-render when state updates, then you are using the wrong state management pattern, flat-out. Instead you should use something like Redux, which removes "state" from the React component tree entirely, and allows components to directly subscribe to state updates.
Redux will allow only the component that subscribes to specific store values to update only when those values update. So, your Parent component and the Child component that dispatches the update action won't update, while only the Child component that subscribes to the state updates. It's very efficient!
https://codesandbox.io/s/simple-redux-example-y3t32
React component is updated only when either
Its own props is changed
state is changed
parent's state is changed
As you have pointed out state needs to be saved in the parent component and passed on to the context.
Your requirement is
Parent should not re-render when state is changed.
Only Child1 should re-render on state change
const SomeContext = React.createContext(null);
Child 1 and 2
const Child1 = () => {
const ctx = useContext(SomeContext);
console.log(`child1: ${ctx}`);
return <div>{ctx.value}</div>;
};
const Child2 = () => {
const ctx = useContext(UpdateContext);
console.log("child 2");
const onClickBtn = () => {
ctx.updateValue("updates");
};
return <button onClick={onClickBtn}>Update Value </button>;
};
Now the context provider that adds the state
const Provider = (props) => {
const [state, setState] = useState({ value: "Hello" });
const updateValue = (newValue) => {
setState({
value: newValue
});
};
useEffect(() => {
document.addEventListener("stateUpdates", (e) => {
updateValue(e.detail);
});
}, []);
const getState = () => {
return {
value: state.value,
updateValue
};
};
return (
<SomeContext.Provider value={getState()}>
{props.children}.
</SomeContext.Provider>
);
};
Parent component that renders both the Child1 and Child2
const Parent = () => {
// This is only logged once
console.log("render parent");
return (
<Provider>
<Child1 />
<Child2 />
</Provider>
);
};
Now for the first requirement when you update the state by clicking button from the child2 the Parent will not re-render because Context Provider is not its parent.
When the state is changed only Child1 and Child2 will re-render.
Now for second requirement only Child1 needs to be re-rendered.
For this we need to refactor a bit.
This is where reactivity comes. As long as Child2 is a child of Provider when ever the state changes it will also gets updated.
Take the Child2 out of provider.
const Parent = () => {
console.log("render parent");
return (
<>
<Provider>
<Child1 />
</Provider>
<Child2 />
</>
);
};
Now we need some way to update the state from Child2.
Here I have used the browser custom event for simplicity. You can use RxJs.
Provider is listening the state updates and Child2 will trigger the event when button is clicked and state gets updated.
const Provider = (props) => {
const [state, setState] = useState({ value: "Hello" });
const updateValue = (e) => {
setState({
value: e.detail
});
};
useEffect(() => {
document.addEventListener("stateUpdates", updateValue);
return ()=>{
document.addEventListener("stateUpdates", updateValue);
}
}, []);
return (
<SomeContext.Provider value={state}>{props.children}</SomeContext.Provider>
);
};
const Child2 = () => {
console.log("child 2");
const onClickBtn = () => {
const event = new CustomEvent("stateUpdates", { detail: "Updates" });
document.dispatchEvent(event);
};
return <button onClick={onClickBtn}>Update Value </button>;
};
NOTE: Child2 will not have access to context
I hope this helps let me know if you didn't understand anything.
I am practicing reactjs and redux course. I am understanding the react part and also redux but could not grasp the knowledge of selector and binding done in below code.
Here is the code
const reducer = (state = 1, action) => {
switch (action.type) {
case 'INCREASE':
return state + 1;
default:
return state;
}
}
const selectCounter = state => state;
const AppPresentation = ({ text, onClick }) => (
<button onClick={onClick}>{text}</button>
);
const App = connect(
(state, { bindings: { selectText } }) => ({ text: selectText(state) }),
dispatch => ({ onClick() { dispatch({ type: 'BUTTON_CLICKED' }); }})
)(AppPresentation)
const onClickIncrease = function*(){
while (yield take('BUTTON_CLICKED'))
yield put({ type: 'INCREASE' });
}
const saga = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(saga)));
saga.run(onClickIncrease);
ReactDOM.render(
<Provider store={store}>
<App bindings={{ selectText: selectCounter }} />
</Provider>,
document.querySelector('#app'));
what is the advantage of above code over below code where selector and bindings has not done?
const reducer = (state = 1, action) => {
switch (action.type) {
case 'INCREASE':
return state + 1;
default:
return state;
}
}
const selectCounter = state => state;
const AppPresentation = ({ text, onClick }) => (
<button onClick={onClick}>{text}</button>
);
const App = connect(
state => ({ text: selectCounter(state) }),
dispatch => ({ onClick() { dispatch({ type: 'BUTTON_CLICKED' }); }})
)(AppPresentation)
const onClickIncrease = function*(){
while (yield take('BUTTON_CLICKED'))
yield put({ type: 'INCREASE' });
}
const saga = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(saga)));
saga.run(onClickIncrease);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector('#app'));
Can anyone please make me understand with simple English explanation? :) Sorry my English is poor.
mapStateToProps has the follwoing syntax mapStateToProps(state, [ownProps])
If ownProps is specified as a second argument, its value will be the props passed to your component, and mapStateToProps will be additionally re-invoked whenever the component receives new props (e.g. if props received from a parent component have shallowly changed, and you use the ownProps argument, mapStateToProps is re-evaluated)
In your case { bindings: { selectText } } will receive the props passed from the parent component
In the first code you are sending a prop to the App like <App bindings={{ selectText: selectCounter }} /> which is received in you mapStateToProps component while in the second code you are not passing any such prop to the component. Hence the first has an advantage over the second to allow you to pass props to the child and update the value in the child when the prop changes.