I'm building a simple review app with react and redux toolkit.
Reviews are added via a form in AddReview.js, and I'm wanting to display these reviews in Venue.js.
When I submit a review in AddReview.js, the new review is added to state, as indicated in redux dev tools:
However when I try to pull that state from the store in Venue.js, I only get the initial state (the first two reviews), and not the state I've added via the submit form:
Can anyone suggest what's going wrong here?
Here's how I've set up my store:
store.js
import { configureStore } from "#reduxjs/toolkit";
import reviewReducer from '../features/venues/venueSlice'
export const store = configureStore({
reducer:{
reviews: reviewReducer
}
})
Here's the slice managing venues/reviews:
venueSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = [
{id:1, title: 'title 1',blurb: 'blurb 1'},
{id:2, title: 'title 2',blurb: 'blurb 2'}
]
const venueSlice = createSlice({
name: 'reviews',
initialState,
reducers: {
ADD_REVIEW: (state,action) => {
state.push(action.payload)
}
}
})
export const { ADD_REVIEW } = venueSlice.actions
export default venueSlice.reducer
And here's the Venue.js component where I want to render reviews:
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
const Venue = () => {
const { id } = useParams()
const reviews = useSelector((state) => state.reviews)
console.log(reviews)
return (
<div>
{reviews.map(item => (
<h1>{item.title}</h1>
))}
</div>
)
}
export default Venue;
Form component AddReview.js
import { useState } from "react"
import { useDispatch } from "react-redux"
import { ADD_REVIEW } from "./venueSlice"
import { nanoid } from "#reduxjs/toolkit"
const AddReview = () => {
const [ {title,blurb}, setFormDetails ] = useState({title:'', blurb: ''})
const dispatch = useDispatch()
const handleChange = (e) => {
const { name, value } = e.target
setFormDetails(prevState => ({
...prevState,
[name]: value
}))
}
const handleSubmit = (e) => {
console.log('it got here')
e.preventDefault()
if(title && blurb){
dispatch(ADD_REVIEW({
id: nanoid(),
title,
blurb
}))
// setFormDetails({title: '', blurb: ''})
}
}
return(
<div>
<form onSubmit={handleSubmit}>
<input
type = 'text'
name = 'title'
onChange={handleChange}
/>
<input
type = 'text'
name = 'blurb'
onChange={handleChange}
/>
<button type = "submit">Submit</button>
</form>
</div>
)
}
export default AddReview;
I can notice that you pushing directly to the state, I can suggest to use variable in the state and then modify that variable.
Also I suggest to use concat instead of push. Where push will return the array length, concat will return the new array.
When your code in the reducer will looks like that:
import { createSlice } from "#reduxjs/toolkit";
const initialState = [
reviews: [{id:1, title: 'title 1',blurb: 'blurb 1'},
{id:2, title: 'title 2',blurb: 'blurb 2'}]
]
const venueSlice = createSlice({
name: 'reviews',
initialState,
reducers: {
ADD_REVIEW: (state,action) => {
state.reviews = state.reviews.concat(action.payload);
}
}
})
export const { ADD_REVIEW } = venueSlice.actions
export default venueSlice.reducer
And then your selector will looks like that:
const reviews = useSelector((state) => state.reviews.reviews)
Your code seems to be fine. I don't see any reason why it shouldn't work.
I run your code on stackblitz react template and its working as expected.
Following is the link to the app:
stackblitz react-redux app
Link to the code:
Project files react-redux
if you are still unable to solve the problem, do create the sandbox version of your app with the issue to help further investigate.
Thanks
Expanding on #electroid answer (the solution he provided should fix your issue and here is why):
Redux toolkit docs mention on Rules of Reducers :
They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
and on Reducers and Immutable Updates :
One of the primary rules of Redux is that our reducers are never allowed to mutate the original / current state values!
And as mdn docs specify the push method changes the current array (so it mutates your state). You can read more about mutating the state in the second link link (Reducers and Immutable Updates).
If you really want to keep the state.reviews and avoid state.reviews.reviews you could also do something like this:
ADD_REVIEW: (state,action) => {
state = [...state, action.payload];
}
But I wouldn't recommend something like this in a real app (it is avoided in all the examples you can find online). Some reason for this would be:
It harder to work with, read and track the state when having an overall dynamic state instead of a state structure
It leads to a lot of slices in a real app (creating a slices for an array without grouping the data) which can also become hard to track and maintain.
Usually you need a redux slice in multiple parts of the app (otherwise you can just use state). That data is usually bigger than just an array and not grouping the data properly on reducers can become very very confusing.
But I would definitely advise to use something else (not reviews.reviews). In your case I think something like state.venue.reviews
(so on store.js
...
export const store = configureStore({
reducer:{
venue: reviewReducer // reviewReducer should probably also be renamed to venueSlice or venueReducer
}
})
So an option to avoid state.venue.reviews or state.reviews.reviews would be to export a selector from the venueSlice.js:
export const selectReviews = (state) => state.venue.reviews
and in your Venue.js component you can just use
const reviews = useSelector(selectReviews)
Exporting a selector is actually suggested by the redux toolkit tutorials as well (this link is for typescript but the same applies to javascript). Although this is optional.
Related
I have since modified my code to focus on the problem so its more easier to understand by whoever attempts to help.
Scenario
I have a redux store created with Redux toolkit with a slice named 'asts' and initialized with 'astsTestData' array.
import { createSlice } from "#reduxjs/toolkit";
import { astsData, astsTestData } from "../data/astsData/astsData";
const astsSlice = createSlice({
name: "asts",
initialState: { astsData, astsTestData },
reducers: {
astCreated: (state, action) => {
state.astsData.push(action.payload);
},
astUpdated: (state, action) => {},
astDeleted: (state, action) => {},
astTestDataCreated: (state, action) => {
console.log(`astTestDataCreated running`);
console.log(`state.astsTestData`, state.astsTestData);
console.log(`action`, action);
return {
...state,
astsTestData: [...astsTestData, action.payload],
};
// state.astsTestData.push(action.payload)
},
},
});
// console.log(`astsSlice`, astsSlice)
export const { astCreated, astUpdated, astDeleted, astTestDataCreated } =
astsSlice.actions;
export default astsSlice.reducer;
I have another slice named 'sch'. This slice stores rgv data.
import { createSlice } from "#reduxjs/toolkit";
import { poData, splData, grvData } from "../data/schData/schData";
const schSlice = createSlice({
name: "sch",
initialState: { poData, splData, grvData },
reducers: {
// Purchase Order reducers
// Goods receiving reducers
grvCreated: (state, action) => {
console.log(`grvCreated running`);
console.log(`state.grvData`, state.grvData);
console.log(`action`, action);
state.grvData.push(action.payload);
},
grvUpdated: (state, action) => {},
grvDeleted: (state, action) => {},
},
});
// console.log(`schSlice`, schSlice);
export const {
poCreated,
poUpdated,
poDeleted,
popCreated,
popUpdated,
popDeleted,
grvCreated,
grvUpdated,
grvDeleted,
} = schSlice.actions;
export default schSlice.reducer;
I have a react component called 'Sch' that displays 'sch.grvData' on a ag-grid table. 'Sch uses 'ag-grid' on a component called 'GrvTestTable' to display 'sch.rgvData' acquired using useSelctor. This part works well all the time.
import React from "react";
import GrvTestAddAstBtn from "../../components/forms/grvForm/grvTest/GrvTestAddAstBtn";
import GrvTestTable from "../../components/forms/grvForm/grvTest/GrvTestTable";
const Sch = () => {
return (
<div className="sch">
<GrvTestTable />
<GrvTestAddAstBtn />
</div>
);
};
export default Sch;
The table displays 'sch.grvData' records that are created via dispatch from "handlSubmit" on grv form .
const handleSubmit = e => {
e.preventDefault();;
// dispatch data to 'sch.grvData'
dispatch(grvCreated(grvFormData));
// dispatch data to 'asts.astsTestData'
dispatch(
astTestDataCreated({
astId: nanoid(),
grvId: grvFormData.grvId,
astCartegory: grvFormData.grvAstCartegory,
astNo: grvFormData.grvAstNo,
})
);
setModalOpened(false);
setGrvFormData([]);
setComponentToOpen("");
};
From 'Sch', besides the ag grid table, there is a button used to open the 'grv form' where on submission the grv form data is written into the 'sch.rgvData' that's on in redux store after which, same data is used to create a new asts record on 'asts.astsTestData'.
I have another component called 'TestAstTable' that displays the 'asts.astsTestData' using ag data grid table. This is where the problem is.
Both my ag-grid tables ( and ) receive data from the grid store via useSelector.
TestAstTable
import React, { useRef, useMemo, useState, useEffect } from "react";
import { AgGridReact } from "ag-grid-react"; // the AG Grid React Component
import "ag-grid-community/styles/ag-grid.css"; // Core grid CSS, always needed
import "ag-grid-community/styles/ag-theme-alpine.css"; // Optional theme CSS
import "react-tippy/dist/tippy.css";
import { useSelector, useStore } from "react-redux";
const TestAstsTable = () => {
const { astsTestData } = useSelector(state => state.asts)
console.log(`astsTestData`, astsTestData);
const [rowData, setRowData] = useState(astsTestData)
const [columnDefs] = useState([
{ field: "astId" },
{ field: "grvId" },
{ field: "astCartegory" },
{ field: "astNo"},
])
useEffect(() => {
setRowData(astsTestData);
}, [astsTestData]);
const gridRef = useRef();
const defaultColDef = useMemo(
() => ({
sortable: true,
filter: true,
resizable: true,
}),
[]
);
// console.log(`rowData`, rowData);
return (
<div className={`ag-theme-alpine `}>
<AgGridReact
ref={gridRef} // Ref for accessing Grid's API
rowData={rowData} // Row Data for Rows
columnDefs={columnDefs} // Column Defs for Columns
defaultColDef={defaultColDef} // Default Column Properties
animateRows={true} // Optional - set to 'true' to have rows animate when sorted
rowSelection="single" // Options - allows click selection of rows
domLayout={"autoHeight"}
/>
</div>
);
};
export default TestAstsTable;
// TODO: mouse over tips on the TestAstsTable skipHeader
GrvTestTable
import React, { useRef, useMemo, useState, useEffect } from "react";
import { AgGridReact } from "ag-grid-react"; // the AG Grid React Component
import "ag-grid-community/styles/ag-grid.css"; // Core grid CSS, always needed
import "ag-grid-community/styles/ag-theme-alpine.css"; // Optional theme CSS
import "react-tippy/dist/tippy.css";
import { useSelector } from "react-redux";
const GrvTestTable = () => {
const { grvData } = useSelector(state => state.sch);
console.log(`grvData`, grvData);
const [rowData, setRowData] = useState(grvData);
useEffect(() => {
setRowData(grvData)
}, [grvData]);
const [columnDefs] = useState([
{ field: "grvId" },
{ field: "grvAstCartegory" },
{ field: "grvAstNo" },
]);
const gridRef = useRef();
const defaultColDef = useMemo(
() => ({
sortable: true,
filter: true,
resizable: true,
}),
[]
);
// console.log(`rowData`, rowData);
return (
<div className={`ag-theme-alpine `}>
<AgGridReact
ref={gridRef} // Ref for accessing Grid's API
rowData={rowData} // Row Data for Rows
columnDefs={columnDefs} // Column Defs for Columns
defaultColDef={defaultColDef} // Default Column Properties
animateRows={true} // Optional - set to 'true' to have rows animate when sorted
rowSelection="single" // Options - allows click selection of rows
domLayout={"autoHeight"}
/>
</div>
);
};
export default GrvTestTable;
Every time a new 'grv record' is created on 'sch.grvData', there is an ast record created on the 'asts.astsTestData'. This is done on form submit handler (handleSubmit) from 'grv form'.
Through redux devtools, I can can confirm that the store gets updated after I dispatch the action payload from the form submit. Via redux dev tools I can see that both 'sch.rgvData' and 'asts.astsTestData' are updated.
With redux toolkit immer, I'm able to use array push for both 'sch.rgvData' (state.grvData.push(action.payload)) and 'asts.astsTestData' (state.astsTestData.push(action.payload)) in my reducers to update the immutable sate.
Problem
'TestAstTable' table component which uses useSelctor to acquire redux state.astsTestData DOES NOT update but the 'GrvTestTable' does update on every grv form submission.
Efforts I've tried to solve the problem
I've looked all over the web in vain
I've tried useState and useEffect to trigger rerender when astsTestData updates but this does not work.
I've looked to see if I'm not mutating the store, and I am NOT
I thought the problem mmay be immutability so I tried on reducers to use old way with immer, no luck. With immer I to push the acton payload on the array. With the old way I used object spread operator. no luck.
I verified that I'm using only one store
Some weird observations
When I look at 'asts' redux page using redux dev tools I don't see updated asts state in redux store, but when I look at 'Sch' redux page using redux dev tools I do see both 'sch.grvData' and 'asts.astsTestData' updated state in redux store. This left me very confused.
It's hard to help you out without a reproducible example in stackblitz etc.
I have a very simple redux boilerplate code that works fine if I use action?.type and not when I use action.type. My understanding is that the action is never null right? So, any idea why I'm getting the error:
Cannot read properties of undefined (reading 'type')
The way I've created the project is I have an action export const NAME_UPDATED = 'profile/updateName'; and an action creator for this action i.e. export function updateProfile(text) { return { type: NAME_UPDATED, payload: text }}.
My reducer is:
import { NAME_UPDATED } from './actions';
//initial state
const initialState = { name: 'Initial Name' }
//reducers
export function myReducer(state = initialState, action) {
switch (action?.type) {
case NAME_UPDATED: return { ...state, name: action.payload };
default: return state;
}
}
I'm dispatching the actions on a button press:
// Imports
const myDispatch = useDispatch();
const mySelector = useSelector(myReducer);
// Other code
<p>
The Current value of <code>name</code> in the store is: <code>{mySelector.name}</code>.
</p>
<button onClick={() => myDispatch(updateProfile('Updated Name'))}>
Learn React
</button>
// Other code
Here is the code sand box for more info: https://codesandbox.io/embed/redux-basic-example-ftrvs0?autoresize=1&fontsize=14&hidenavigation=1&theme=dark
useSelector allows you to extract data from the Redux store state, using a selector function. https://react-redux.js.org/api/hooks#useselector
In your case:
const mySelector = useSelector((store) => store)
I haven't been in React for a while and now I am revising. Well I faced error and tried debugging it for about 2hours and couldn't find bug. Well, the main logic of program goes like this:
There is one main context with cart object.
Main property is cart array where I store all products
If I add product with same name (I don't compare it with id's right now because it is small project for revising) it should just sum up old amount of that product with new amount
Well, I did all logic for adding but the problem started when I found out that for some reason when I continue adding products, it linearly doubles it up. I will leave github link here if you want to check full aplication. Also, there I will leave only important components. Maybe there is small mistake which I forget to consider. Also I removed logic for summing up amount of same products because that's not neccesary right now. Pushing into state array is important.
Github: https://github.com/AndNijaz/practice-react-
//Context
import React, { useEffect, useReducer, useState } from "react";
const CartContext = React.createContext({
cart: [],
totalAmount: 0,
totalPrice: 0,
addToCart: () => {},
setTotalAmount: () => {},
setTotalPrice: () => {},
});
const cartAction = (state, action) => {
const foodObject = action.value;
const arr = [];
console.log(state.foodArr);
if (action.type === "ADD_TO_CART") {
arr.push(foodObject);
state.foodArr = [...state.foodArr, ...arr];
return { ...state };
}
return { ...state };
};
export const CartContextProvider = (props) => {
const [cartState, setCartState] = useReducer(cartAction, {
foodArr: [],
totalAmount: 0,
totalPrice: 0,
});
const addToCart = (foodObj) => {
setCartState({ type: "ADD_TO_CART", value: foodObj });
};
return (
<CartContext.Provider
value={{
cart: cartState.foodArr,
totalAmount: cartState.totalAmount,
totalPrice: cartState.totalAmount,
addToCart: addToCart,
}}
>
{props.children}
</CartContext.Provider>
);
};
export default CartContext;
//Food.js
import React, { useContext, useState, useRef, useEffect } from "react";
import CartContext from "../../context/cart-context";
import Button from "../ui/Button";
import style from "./Food.module.css";
const Food = (props) => {
const ctx = useContext(CartContext);
const foodObj = props.value;
const amountInput = useRef();
const onClickHandler = () => {
const obj = {
name: foodObj.name,
description: foodObj.description,
price: foodObj.price,
value: +amountInput.current.value,
};
console.log(obj);
ctx.addToCart(obj);
};
return (
<div className={style["food"]}>
<div className={style["food__info"]}>
<p>{foodObj.name}</p>
<p>{foodObj.description}</p>
<p>{foodObj.price}$</p>
</div>
<div className={style["food__form"]}>
<div className={style["food__form-row"]}>
<p>Amount</p>
<input type="number" min="0" ref={amountInput} />
</div>
<Button type="button" onClick={onClickHandler}>
+Add
</Button>
</div>
</div>
);
};
export default Food;
//Button
import style from "./Button.module.css";
const Button = (props) => {
return (
<button
type={props.type}
className={style["button"]}
onClick={props.onClick}
>
{props.children}
</button>
);
};
export default Button;
Issue
The React.StrictMode component is exposing an unintentional side-effect.
See Detecting Unexpected Side Effects
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer <-- here
The function passed to useReducer is double invoked.
const cartAction = (state, action) => {
const foodObject = action.value;
const arr = [];
console.log(state.foodArr);
if (action.type === "ADD_TO_CART") {
arr.push(foodObject); // <-- mutates arr array, pushes duplicates!
state.foodArr = [...state.foodArr, ...arr]; // <-- duplicates copied
return { ...state };
}
return { ...state };
};
Solution
Reducer functions are to be considered pure functions, taking the current state and an action and compute the next state. In the sense of pure functionality, the same next state should result from the same current state and action. The solution is only add the new foodObject object once, based on the current state.
Note also for the default "case" just return the current state object. Shallow copying the state without changing any data will unnecessarily trigger rerenders.
I suggest also renaming the reducer function to cartReducer so its purpose is more clear to future readers of your code.
const cartReducer = (state, action) => {
switch(action.type) {
case "ADD_TO_CART":
const foodObject = action.value;
return {
...state, // shallow copy current state into new state object
foodArr: [
...state.foodArr, // shallow copy current food array
foodObject, // append new food object
],
};
default:
return state;
}
};
...
useReducer(cartReducer, initialState);
Additional Suggestions
When adding an item to the cart, first check if the cart already contains that item, and if so, shallow copy the cart and the matching item and update the item's value property which appears to be the quantity.
Cart/item totals are generally computed values from existing state. As such these are considered derived state and they don't belong in state, these should computed when rendering. See Identify the minimal (but complete) representation of UI state. They can be memoized in the cart context if necessary.
I have problem when trying to send data through the function action in redux,
my code is below
import React from 'react'
import {connect} from 'react-redux'
import {RetrieveCompany} from '../../folder/action/my.actions'
interface Icampaing{
campaing: my_data
}
// campaing IS WORKING WELL, GET ME ALL MY DATA
const Personal: React.FC<Icampaing> = ({campaing}, props: nay) => {
React.useEffect(()=>{
let pf_id: any = campaing.profile ? campaing.profile.id : 0
let pc_id: any = campaing.profile_ca
// THE PROBLEM IS HERE SHOW ME THE ERROR
// TypeError: props.RetrieveCompany is not a function
props.RetrieveCompany(pf_id, pc_id)
},[campaing])
return(<>
{campaing.all_data} // HERE WHEN LOAD DATA campaing WORKING WELL
</>)
}
const mapStateToProps = (state: any) =>({
campaing: state.campaing
})
const mapActionToProps = {
RetrieveCompany
}
export default connect(mapStateToProps, mapActionToProps)(Personal)
please help me, I think forget something.
best words, and happy new year.....!
You should use mapDispatchToProps instead of mapActionToProps
const mapDispatchToProps = (dispatch) => ({
RetrieveCompany: () => dispatch(RetrieveCompany())
// Important: this could be just
// RetrieveCompany depends on how you define your action.
// For naming, I would use camelCase
});
Because what you need to do here is to dispatch an action so that the store will update its state. Then you would read the data returned by mapStateToProps.
I think RetrieveCompany is not among props in deed. Try to spread the rest of the props if you do not want to explicitly name it:
interface Icampaing {
campaing: my_data
[propName: string]: any
}
const Personal: React.FC<Icampaing> = ({ campaing, ...props }) => {
...
or simply add it explicitly since you use it in the component anyways:
interface Icampaing {
campaing: my_data
RetrieveCompany: (pf_id: number, pc_id: number) => void
}
const Personal: React.FC<Icampaing> = ({ campaing, RetrieveCompany }) => {
There's a bunch of articles out there that show how Redux can be replaced with context and hooks (see this one from Kent Dodds, for instance). The basic idea is to make your global state available through a context instead of putting it inside a Redux store. But there's one big problem with that approach: components that subscribe to the context will be rerendered whenever any change happens to the context, regardless of whether or not your component cares about the part of the state that just changed. For functional components, React-redux solves this problem with the useSelector hook. So my question is: can a hook like useSelector be created that would grab a piece of the context instead of the Redux store, would have the same signature as useSelector, and, just like useSelector, would only cause rerenders to the component when the "selected" part of the context has changed?
(note: this discussion on the React Github page suggests that it can't be done)
No, it's not possible. Any time you put a new context value into a provider, all consumers will re-render, even if they only need part of that context value.
That's specifically one of the reasons why we gave up on using context to propagate state updates in React-Redux v6, and switched back to using direct store subscriptions in v7.
There's a community-written React RFC to add selectors to context, but no indication the React team will actually pursue implementing that RFC at all.
As markerikson answers, it is not possible, but you can work around it without using external dependencies and without falling back to doing manual subscriptions.
As a workaround, you can let the component re-render, but skip the VDOM reconciliation by memoizing the returned React element with useMemo.
function Section(props) {
const partOfState = selectPartOfState(useContext(StateContext))
// Memoize the returned node
return useMemo(() => {
return <div>{partOfState}</div>
}, [partOfState])
}
This is because internally, when React diffs 2 versions of virtual DOM nodes, if it encountered the exact same reference, it will skip reconciling that node entirely.
I created a toolkit for managing state using ContextAPI. It provides useSelector (with autocomplete) as well as useDispatch.
The library is available here:
https://www.npmjs.com/package/react-context-toolkit
https://github.com/bergkvist/react-context-toolkit
It uses:
use-context-selector to avoid unneccesary rerenders.
createSlice from #reduxjs/toolkit to make the state more modular and to avoid boilerplate.
I've created this small package, react-use-context-selector, and it just does the job.
I used the same approach as used in Redux's useSelector. It also comes with type declarations and the return type matches the selector function's return type making it suitable for using in TS project.
function MyComponent() {
// This component will re-render only when the `name` within the context object changes.
const name = useContextSelector(context, value => value.name);
return <div>{name}</div>;
}
Here is my take on this problem:
I used the function as child pattern with useMemo to create a generic selector component:
import React, {
useContext,
useReducer,
createContext,
Reducer,
useMemo,
FC,
Dispatch
} from "react";
export function createStore<TState>(
rootReducer: Reducer<TState, any>,
initialState: TState
) {
const store = createContext({
state: initialState,
dispatch: (() => {}) as Dispatch<any>
});
const StoreProvider: FC = ({ children }) => {
const [state, dispatch] = useReducer(rootReducer, initialState);
return (
<store.Provider value={{ state, dispatch }}>{children}</store.Provider>
);
};
const Connect: FC<{
selector: (value: TState) => any;
children: (args: { dispatch: Dispatch<any>; state: any }) => any;
}> = ({ children, selector }) => {
const { state, dispatch } = useContext(store);
const selected = selector(state);
return useMemo(() => children({ state: selected, dispatch }), [
selected,
dispatch,
children
]);
};
return { StoreProvider, Connect };
}
Counter component:
import React, { Dispatch } from "react";
interface CounterProps {
name: string;
count: number;
dispatch: Dispatch<any>;
}
export function Counter({ name, count, dispatch }: CounterProps) {
console.count("rendered Counter " + name);
return (
<div>
<h1>
Counter {name}: {count}
</h1>
<button onClick={() => dispatch("INCREMENT_" + name)}>+</button>
</div>
);
}
Usage:
import React, { Reducer } from "react";
import { Counter } from "./counter";
import { createStore } from "./create-store";
import "./styles.css";
const initial = { counterA: 0, counterB: 0 };
const counterReducer: Reducer<typeof initial, any> = (state, action) => {
switch (action) {
case "INCREMENT_A": {
return { ...state, counterA: state.counterA + 1 };
}
case "INCREMENT_B": {
return { ...state, counterB: state.counterB + 1 };
}
default: {
return state;
}
}
};
const { Connect, StoreProvider } = createStore(counterReducer, initial);
export default function App() {
return (
<StoreProvider>
<div className="App">
<Connect selector={(state) => state.counterA}>
{({ dispatch, state }) => (
<Counter name="A" dispatch={dispatch} count={state} />
)}
</Connect>
<Connect selector={(state) => state.counterB}>
{({ dispatch, state }) => (
<Counter name="B" dispatch={dispatch} count={state} />
)}
</Connect>
</div>
</StoreProvider>
);
}
Working example: CodePen
Solution with external store (Redux or Zustand like approach) with new hook useSyncExternalStore comes with React 18.
For React 18: Define createStore and useStore functions:
import React, { useCallback } from "react";
import { useSyncExternalStore } from "react";
const createStore = (initialState) => {
let state = initialState;
const getState = () => state;
const listeners = new Set();
const setState = (fn) => {
state = fn(state);
listeners.forEach((l) => l());
};
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
return { getState, setState, subscribe };
};
const useStore = (store, selector) =>
useSyncExternalStore(
store.subscribe,
useCallback(() => selector(store.getState()), [store, selector])
);
Now use it :
const store = createStore({ count: 0, text: "hello" });
const Counter = () => {
const count = useStore(store, (state) => state.count);
const inc = () => {
store.setState((prev) => ({ ...prev, count: prev.count + 1 }));
};
return (
<div>
{count} <button onClick={inc}>+1</button>
</div>
);
};
For React 17 and any React version that supports hooks:
Option 1: You may use the external library (maintained by React team)
use-sync-external-store/shim :
import { useSyncExternalStore } from "use-sync-external-store/shim";
Option 2: If you don't want to add new library and don't care about concurency problems:
const createStore = (initialState) => {
let state = initialState;
const getState = () => state;
const listeners = new Set();
const setState = (fn) => {
state = fn(state);
listeners.forEach((l) => l());
}
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
}
return {getState, setState, subscribe}
}
const useStore = (store, selector) => {
const [state, setState] = useState(() => selector(store.getState()));
useEffect(() => {
const callback = () => setState(selector(store.getState()));
const unsubscribe = store.subscribe(callback);
callback();
return unsubscribe;
}, [store, selector]);
return state;
}
Sources:
A conference talk from Daishi Kato from React Conf 2021
A blog post about same conference talk by Chetan Gawai
Simple approach to prevent additional renders with HoC and React.memo:
const withContextProps = (WrappedComponent) => {
const MemoizedComponent = React.memo(WrappedComponent);
return (props) => {
const state = useContext(myContext);
const mySelectedState = state.a.b.c;
return (
<MemoizedComponent
{...props}
mySelectedState={mySelectedState} // inject your state here
/>
);
};
};
withContextProps(MyComponent)
I have made a library, react-context-slices, which can solve what you are looking for. The idea is to break the store or state in slices of state, that is, smaller objects, and create a context for each one. That library which I told you does this, exposes a function createSlice which accepts a reducer, initial state, name of the slice, and a function for creating the actions. You create as slices as you want ('todos', 'counter', etc) and integrate them in a unique interface easily, exposing at the end two custom hooks, useValues and useActions, which can 'attack' all the slices (that is, in your client components you do not use useTodosValues but useValues). The key is that useValues accepts a name of the slice, so would be equivalent to the useSelector from redux. The library use immer as redux does. It's a very tiny library which the key point is how is used, which is explained in the readme file. I have also made a post about it. The library exposes only two functions, createSlice and composeProviders.