I'm using Redux toolkit in a React.js app, and I extracted my logic as below:
Store.tsx
const history = createHashHistory();
const middleware = [
...getDefaultMiddleware().concat(routerMiddleware(history)),
];
const Store = configureStore({
reducer: {
router: connectRouter(history) as Reducer<any, any>,
/* my reducers */
}, middleware
});
MySlice.tsx
import {createSlice} from '#reduxjs/toolkit';
const initialState = {
/* content of my state */
};
const mySlice = createSlice(
{
name: "myState",
initialState,
reducers: {
myAction: (state: MyState) => {
// Whatever here...
}
},
extraReducers: builder => {
/* My extra reducers */
}
});
export const MySlice = mySlice;
And then I have my function:
MySuperFunc.tsx
export const superFunc = () => {
/* content of my function */
const {myAction} = MySlice.actions;
Store.dispatch(myAction({my: 'payload'}));
};
I would like to unit test it with Jest. I want to mock the content of my Store/MySlice because the configureStore & createSlice doing extra logics and seems to require some configuration.
I'm a little lost from React.js best practices & documentation regarding mock, setMock and spyOn.
superFunc.spec.ts
const dispatchMockFn = jest.fn();
// Sol: 1
jest.mock('<< path >>/Store', () => ({dispatch: dispatchMockFn )});
// Sol: 2
jest.setMock('<< path >>/Store', {dispatch: dispatchMockFn});
// Sol: 3
jest.spyOn(Store, 'dispatch');
describe('superFunc', () => {
it('should call store', () => {
superFunc();
return expect(dispatchMockFn).toHaveBeenCalledWith(/* results of myAction({my: 'payload'}) */);
});
});
The problem I faced some error by code executed in the Store:
Seems to be normal because I used "Store" which is not exporting only an object, we have some extra code inside (createHistory etc.).
I searched a solution to mock entirely a module, that's why I try setMock/mock, it change a little bit the error but know it complaining regarding MySlice (extraReducers) saying that my promises.fulfilled/rejected are not defined etc.
But I don't want to go deep inside the Store/Slice config, I just want to mock the file content.
Any idea/recommendation?
I found the solution:
The jest.mock factory must be adapted from your export (export default VS. export const = {...})
Example 1:
MySuperFunc.tsx (with export default)
export default ({superFunc: () => 'hello world'});
Unit test:
import superFunc from './MySuperFunc';
jest.mock('./MySuperFunc', () => ({superFunc: jest.fn()});
it('should return toto', () => {
(MySuperFunc.superFunc as any).mockReturnValue('toto');
return expect(MySuperFunc.superFunc).toHaveBeenCalledWith('toto');
});
But, in the example 2 if you change your export by:
// MySuperFunc.tsx
export const MyVariable = {superFunc: () => 'hello world'});
Your jest.mock must be adapted as:
jest.mock('./MySuperFunc', () => ({MyVariable: {superFunc: jest.fn()}});
So when I re-take my question, I have 2 choices:
I can change my export in my slice file by using an export default
Or I adapt my jest.mock including {MySlice: {actions: {myAction: () => '...'}}}
My apologies, this topic is clearly not related to "redux toolkit" but more on how you deals with your export content from your typescript files.
I am Learning Redux Toolkit and using createEntityAdapter for a simple todo app.
the problem is that I am getting errors whenever I try to access the todos from globalselectors or/and simple selectors of the todoAdapter.
I've created a CodeSandBox for this
This is the todoReducer code where I am setting
const todosAdapter = createEntityAdapter({
selectId: (todo) => todo._id,
});
const initialState = {
fetching: true,
error: null,
addingNew: false,
};
const todosSlice = createSlice({
name: 'todos',
initialState: todosAdapter.getInitialState(initialState),
reducers: {
pushNewTodo: todosAdapter.addOne,
addManyTodos: todosAdapter.addMany,
removeTodo: todosAdapter.removeOne,
editTodo: todosAdapter.updateOne,
},
extraReducers: { ... }
})
//* Errror in this line ,
export const globalTodosReducers = todosAdapter.getSelectors((st) => st.todos);
export const simpleTodosReducers = todosAdapter.getSelectors();
export const {
addTodo,
removeTodo,
toggleTodo,
editTodo,
clearTodos
} = todosSlice.actions;
export default todosSlice.reducer;
And when I want to use simpleSelectors , I get error in that also
// TodoApp.js
function TodoApp() {
.
.
.
// * Error in these lines
// const todos = globalTodosReducers.selectAll();
const todos = simpleTodosReducers.selectAll();
.
.
.
error is
Cannot read properties of undefined (reading 'ids')
The object returned by getSelectors contains many selector functions. Each one requires you to pass in the state object that contains the data you want to select from. You had the right idea here:
todosAdapter.getSelectors((st) => st.todos)
This tells the entity adapter to access the todos property of whatever object is passed in to the selector functions in order to get the data managed by the adapter.
When you call a selector, pass in your application's root state:
const todos = globalTodosReducers.selectAll(store.getState());
In the context of React, react-redux's useSelector function will do this for you:
const todos = useSelector(globalTodosReducers.selectAll);
I have a Next.js app with Redux. Using 3 library:
Redux Toolkit
React Redux
next-redux-wrapper
After first user interaction I would store data in redux, so I call:
useAppDispatch(setInvoiceItems(itemsWithSelection2));
and it raise an error:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
at Object.throwInvalidHookError (react-dom.development.js?61bb:14906)
at useContext (react.development.js?72d0:1504)
at useReduxContext (useReduxContext.js?9825:21)
This is the whole method inside the function component:
const switchSelection = (key) => {
let itemsWithSelection2;
if (itemsWithSelection) {
itemsWithSelection2 = { ...itemsWithSelection };
} else {
itemsWithSelection2 = Object.fromEntries(
Object.keys(invoiceItemsFiltered)
.filter((key) => invoiceItems[key].defaultValue != undefined)
.map((key) => [key, invoiceItems[key].defaultValue])
);
}
itemsWithSelection2[key] = itemsWithSelection2[key] == 1 ? 0 : 1;
setItemsWithSelection(itemsWithSelection2);
useAppDispatch(setInvoiceItems(itemsWithSelection2));
};
What is wrong in my code?
I store StartPaymentIn type in redux. It has a field invoiceItems, string number pairs.
import { Action, createSlice, PayloadAction } from "#reduxjs/toolkit";
import { StartPaymentIn, InvoiceItemData } from "../../sharedDirectory/Types";
const initialState: StartPaymentIn = {
eventId: "",
hostName: "",
lang: "",
invoiceItems: {},
formFields: {},
};
const StartPaymentInSlice = createSlice({
name: "StartPaymentIn",
initialState,
reducers: {
setInvoiceItems(state, action: PayloadAction<{ InvoiceItemData? }>) {
state.invoiceItems = action.payload;
},
},
});
export const { setInvoiceItems } = StartPaymentInSlice.actions;
export default StartPaymentInSlice.reducer;
export type StartPaymentIn = {
invoiceItems?: InvoiceItemDataOnBuyTicket;
};
export type InvoiceItemDataOnBuyTicket = {
[invoiceItemId: string]: number;
};
const InvoiceItemsToDeliver = (props: ProductProps) => {
let itemsWithSelection2 = useAppSelector((state) => state.invoiceItems);
if (invoiceItems) {
if (!itemsWithSelection2) {
useAppDispatch(setInvoiceItems(invoiceItems2));
}
}
const [itemsWithSelection, setItemsWithSelection] = useState<{
[formFieldId: string]: number;
}>(itemsWithSelection2);
React hook "useAppDispatch" cannot be used inside pure functions. They can only be called inside react component. In your code you are using the useAppDispatch(setInvoiceItems(itemsWithSelection2)); inside a pure function outside of the component.
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.