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.
I am trying to understand and practice React hooks. I have the following code with me.
import React, { useState, useEffect, useReducer } from "react";
import "./App.css";
import DisplayUser from "./comp/DisplayUser";
const initialState = {
firstStep: 0,
};
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { firstStep: state.firstStep + action.payload };
case "decrement":
return { firstStep: state.firstStep - action.payload };
case "reset":
return initialState;
default:
return state;
}
};
export const UserContext = React.createContext();
function App() {
const [count, setCount] = useState(0);
const [tick, setTick] = useState(0);
const [step, dispatch] = useReducer(reducer, initialState);
const increaseCount = () => {
setCount((prevCount) => prevCount + 1);
};
const decreaseCount = () => {
console.log("De");
setCount((prevCount) => prevCount - 1);
};
useEffect(() => {
setTimeout(() => {
setTick((prevCount) => prevCount + 1);
}, 1000);
}, [tick]);
return (
<div className="App">
<section className="AppUseState">
<h2>useState</h2>
<article>
<h4>Count : {count}</h4>
<button onClick={() => increaseCount()}>Tick Plus</button>
<button onClick={() => decreaseCount()}>Tick Minus</button>
</article>
</section>
<section className="AppUseEffect">
<h2>useEffect</h2>
<article>
<h4>Tick : {tick}</h4>
</article>
</section>
<section className="AppUseReducer">
<h2>useReducer</h2>
<article>
<h4>Step : {step.firstStep}</h4>
<button onClick={() => dispatch({ type: "increment", payload: 5 })}>
Tick Plus
</button>
<button onClick={() => dispatch({ type: "decrement", payload: 3 })}>
Tick Minus
</button>
</article>
</section>
<section className="AppUseContext">
<h2>useContext</h2>
<article>
<UserContext.Provider value={{ userName: "Mohit" }}>
<DisplayUser />
</UserContext.Provider>
</article>
</section>
</div>
);
}
export default App;
Whenever a the setTick method in the setTimeout runs, its basically re rendering the DisplayUser component. (I have a console log in the DisplayUser component). My DisplayUser component looks like this
import React, { useContext } from "react";
import { UserContext } from "../App";
function DisplayUser() {
const user = useContext(UserContext);
console.log(user);
return (
<React.Fragment>
<p>Hello from {user.userName}</p>
</React.Fragment>
);
}
export default DisplayUser;
Can someone explain this behaviour to me as to why useEffect is basically re rendering my context api consumer component?
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.
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
|I have the following component based on this:
**WarningModal.js**
import React from 'react';
import ReactDOM from 'react-dom';
import {connect, Provider} from 'react-redux';
import PropTypes from 'prop-types';
import {Alert, No} from './pure/Icons/Icons';
import Button from './pure/Button/Button';
import Modal from './pure/Modal/Modal';
import {setWarning} from '../actions/app/appActions';
import configureStore from '../store/configureStore';
const store = configureStore();
export const WarningModal = (props) => {
const {message, withCleanup} = props;
const [
title,
text,
leave,
cancel
] = message.split('|');
const handleOnClick = () => {
props.setWarning(false);
withCleanup(true);
}
return(
<Modal>
<header>{title}</header>
<p>{text}</p>
<Alert />
<div className="modal__buttons-wrapper modal__buttons-wrapper--center">
<button
onClick={() => withCleanup(false)}
className="button modal__close-button button--icon button--icon-only button--text-link"
>
<No />
</button>
<Button id="leave-warning-button" className="button--transparent-bg" onClick={() => handleOnClick()}>{leave}</Button>
<Button id="cancel-warning-button" onClick={() => withCleanup(false)}>{cancel}</Button>
</div>
</Modal>
);
}
WarningModal.propTypes = {
withCleanup: PropTypes.func.isRequired,
message: PropTypes.string.isRequired,
setWarning: PropTypes.func.isRequired
};
const mapStateToProps = state => {
console.log(state)
return {
isWarning: state.app.isWarning
}
};
const WarningModalContainer = connect(mapStateToProps, {
setWarning
})(WarningModal);
export default (message, callback) => {
const modal = document.createElement('div');
document.body.appendChild(modal);
const withCleanup = (answer) => {
ReactDOM.unmountComponentAtNode(modal);
document.body.removeChild(modal);
callback(answer);
};
ReactDOM.render(
<Provider store={store}>
<WarningModalContainer
message={message}
withCleanup={withCleanup}
/>
</Provider>,
modal
);
};
the issue I have is that 'setWarning' doesn't update the state, it does get called as I have a debugger inside the action and the reducer but the actual property doesn't not change to 'false' when:
props.setWarning(false);
gets called.
I use the following to trigger the custom modal:
const togglePromptCondition =
location.hash === '#access-templates' || location.hash === '#security-groups'
? promptCondition
: isFormDirty || isWarning;
<Prompt message={promptMessage} when={togglePromptCondition} />
To test this even further I have added 2 buttons in the application to toggle 'isWarning' (the state property I am talking about) and it works as expected.
I think that although WarningModal is connected in actual fact it isn't.
REDUCER
...
case SET_WARNING:
console.log('reducer called: ', action)
return {
...state,
isWarning: action.payload
};
...
ACTION
...
export const setWarning = status => {
console.log('action called')
return {
type: SET_WARNING,
payload: status
}
};
...
UPDATE
After having to incorporates the following:
const mapStateToProps = state => {
return {
isWarning: state.app.isWarning
}
};
const mapDispatchToProps = dispatch => {
return {
setWarning: (status) => dispatch({ type: 'SET_WARNING', payload: status })
}
};
I am now getting:
Maybe this could help?
You have to dispatch the actions in the action creator and the type of the action to dispatch should be always string.
Try this
const mapStateToProps = state => {
console.log(state)
return {
isWarning: state.app.isWarning
}
};
const mapDispatchToProps = dispatch => {
console.log(dispatch)
return {
setWarning: (status) => dispatch({ type: 'SET_WARNING', payload: status })
}
};
const WarningModalContainer = connect(mapStateToProps, mapDispatchToProps)(WarningModal);
REDUCER
...
case 'SET_WARNING':
console.log('reducer called: ', action)
return {
...state,
isWarning: action.payload
};
...