How to execute Redux dispatch? - reactjs

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.

Related

Best approach to make non component functions update redux state without passing store.dispatch() as a parameter

So I'm creating my first ReactJS/redux application and I need a little guidance.
I've created a generic apiFetch<T>(method, params) : Promise<T> function which lives in api/apiClient.ts. (Not a React component, but called indirectly from React components)
Basically every fetchEmployee/fetchSettings/fetchWhatever method in rpc/rpcMethods.ts calls this apiFetch<T>() function.
What I'd like to achieve is a statusbar in my app which shows how many concurrent api calls are active. I therefore created a redux rpcStatusSlice based on this redux example.
Can I make apiFetch<T>() update the slice without passing the UseAppDispatch() result as a parameter from my React components?
If I directly import the store in apiClient.ts and call the state modifying functions from rpcStatusSlice on it I get this error:
Uncaught ReferenceError: Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization
at Module.default (bundle.js:1444:42)
at Module../src/store/store.ts (bundle.js:1957:67)
at Module.options.factory (bundle.js:90091:31)
at __webpack_require__ (bundle.js:89541:33)
at fn (bundle.js:89762:21)
at Module../src/api/apiClient.ts (bundle.js:206:70)
at Module.options.factory (bundle.js:90091:31)
at __webpack_require__ (bundle.js:89541:33)
at fn (bundle.js:89762:21)
at Module../src/api/rpcMethods.ts (bundle.js:288:68)
apiFetch.ts:
import { store } from "../store/store";
import { incrementByAmount } from "../store/features/rpcStatusSlice";
export function apiFetch<T>(method: string, params: any): Promise<T> {
store.dispatch(incrementByAmount(1));
return fetch(apiUrl, {
method: "POST",
cache: "no-cache",
mode: "cors",
redirect: "follow",
body: JSON.stringify(getApiRequest(method, params)),
})
.then(etc)
./store/features/rpcStatusSlice.ts
import { createSlice, PayloadAction } from '#reduxjs/toolkit';
import { RootState } from '../store';
export interface ActiveRequest{
requestType: string;
}
export interface RpcStatus {
activeRequestsCount: 0;
activeRequests: ActiveRequest[];
}
export interface RpcStatusState {
value: RpcStatus;
status: 'idle' | 'loading' | 'failed';
}
const initialState: RpcStatusState = {
value: {
activeRequestsCount: 0,
activeRequests: []
},
status: 'idle',
};
export const rpcStatusSlice = createSlice({
name: 'rpcstatus',
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value.activeRequestsCount += 1;
},
decrement: (state) => {
state.value.activeRequestsCount -= 1;
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value.activeRequestsCount += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = rpcStatusSlice.actions;
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.rpcstatus.value)`
export const selectCount = (state: RootState) => state.rpcStatus.value.activeRequestsCount;
export default rpcStatusSlice.reducer;
./store/store.ts
import { configureStore, ThunkAction, Action } from '#reduxjs/toolkit';
import rpcStatusReducer from './features/rpcStatusSlice';
export const store = configureStore({
reducer: {
rpcStatus: rpcStatusReducer
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
Can I make apiFetch() update the slice without passing the UseAppDispatch() result as a parameter from my React components?
This sounds like you want apiFetch() to do two things:
Make an HTTP request.
Update redux state.
This violates the separation of concerns principle. Instead, call apiFetch() from a thunk that then updates redux state.
If your StatusBar component is the only consumer of the number of concurrent api calls, I'd consider not even using redux for this. This counter is not necessarily even global state, I'd see it as volatile, local component state for the StatusBar. Therefore, the solution I'm proposing relies only on React. It takes advantage of the pattern of keeping state in the module scope of the apiClient. At runtime, your apiClient module exists as a singleton with its own scope (many modules importing the apiClient will always use the same instance).
This abstracts the task of counting api calls away and lets the other parts of the codebase use apiFetch() without worrying about the counter. No need to adjust your existing thunks etc..
// apiClient.ts
let incrementCounter = (): void => null;
let decrementCounter = (): void => null;
export const onRequestStart = (callback: () => void) => {
incrementCounter = callback;
};
export const onRequestEnd = (callback: () => void) => {
decrementCounter = callback;
};
export const apiFetch = (method, params) => {
incrementCounter();
// do actual api call
return someKindOfPromise.finally(decrementCounter);
};
// StatusBar.tsx (component showing the number of concurrent api calls)
import { onRequestStart, onRequestEnd } from 'apiClient.ts';
const StatusBar = () => {
const [numConcurrentCalls, setNumConcurrentCalls] = useState(0);
useEffect(() => {
// Tell api client to always call the following function when a request starts.
onRequestStart(() => setNumConcurrentCalls(num => num + 1));
// Tell api client to always call the following function when a request ends.
onRequestEnd(() => setNumConcurrentCalls(num => num - 1));
}, [setNumConcurrentApiCalls]);
return (
<div>
<p>Concurrent api calls: {numConcurrentCalls}</p>
</div>
);
};

Cannot read properties of undefined (reading 'ids') , while using globalSelectors,simpleSelectors of redux toolkit in createEntityAdapter

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);

How to push to array value of reducer state in redux toolkit?

I have a simple reducer function
import { createSlice, PayloadAction } from '#reduxjs/toolkit';
import { TSnackBarProps } from 'plugins/notification/NotificationContext';
import { MAX_STACK } from 'plugins/notification/NotificationsStack';
interface INotificationState {
notifications: TSnackBarProps[];
}
const initialState: INotificationState = {
notifications: [],
};
const notificationSlice = createSlice({
name: 'notification',
initialState,
reducers: {
addNewNotification(state, action: PayloadAction<TSnackBarProps>) {
const { notifications } = state;
const { payload: notification } = action;
if (notifications.find((n) => n.severity === notification.severity && n.key === notification.key)) {
return;
}
if (notifications.length >= MAX_STACK) {
notifications.splice(0, notifications.length - MAX_STACK);
}
state.notifications.push(notification);
},
},
});
export default notificationSlice.reducer;
But, it throws the error as shown below:
I am just starting to write this reducer and got stuck here. Thanks for your help.
Also, TSnackBarProps is just SnackBarProps type from material-ui with severity property added.
immer's Draft type, which is used by RTK removes the readonly temporarily from all state types so that you can freely modify it. Unfortunately, that goes a little bit too far in this case.
But of course: you know better than TypeScript here. So you could just cast your state variable, which is Draft<INotificationState> at the moment to INotificationState to assign to it, which is a perfectly valid thing to do in a situation like this.

React FC Context & Provider Typescript Value Issues

Trying to implement a global context on an application which seems to require that a value is passed in, the intention is that an API will return a list of organisations to the context that can be used for display and subsequent API calls.
When trying to add the <Provider> to App.tsx the application complains that value hasn't been defined, whereas I'm mocking an API response with useEffect().
Code as follows:
Types types/Organisations.ts
export type IOrganisationContextType = {
organisations: IOrganisationContext[] | undefined;
};
export type IOrganisationContext = {
id: string;
name: string;
};
export type ChildrenProps = {
children: React.ReactNode;
};
Context contexts/OrganisationContext.tsx
export const OrganisationContext = React.createContext<
IOrganisationContextType
>({} as IOrganisationContextType);
export const OrganisationProvider = ({ children }: ChildrenProps) => {
const [organisations, setOrganisations] = React.useState<
IOrganisationContext[]
>([]);
React.useEffect(() => {
setOrganisations([
{ id: "1", name: "google" },
{ id: "2", name: "stackoverflow" }
]);
}, [organisations]);
return (
<OrganisationContext.Provider value={{ organisations }}>
{children}
</OrganisationContext.Provider>
);
};
Usage App.tsx
const { organisations } = React.useContext(OrganisationContext);
return (
<OrganisationContext.Provider>
{organisations.map(organisation => {
return <li key={organisation.id}>{organisation.name}</li>;
})}
</OrganisationContext.Provider>
);
Issue #1:
Property 'value' is missing in type '{ children: Element[]; }' but required in type 'ProviderProps<IOrganisationContextType>'.
Issue #2:
The list is not rendering on App.tsx
Codesandbox: https://codesandbox.io/s/frosty-dream-07wtn?file=/src/App.tsx
There are a few different things that you'll need to look out for in this:
If I'm reading the intention of the code properly, you want to render OrganisationProvider in App.tsx instead of OrganisationContext.Provider. OrganisationProvider is the custom wrapper you have for setting the fake data.
Once this is fixed, you're going to run into an infinite render loop because in the OrganisationProvider component, the useEffect sets the organisations value, and then runs whenever organisations changes. You can probably set this to an empty array value [] so the data is only set once on initial render.
You're trying to use the context before the provider is in the tree above it. You'll need to re-structure it so that the content provider is always above any components trying to consume context. You can also consider using the context consumer component so you don't need to create another component.
With these suggested updates, your App.tsx could look something like the following:
import * as React from "react";
import "./styles.css";
import {
OrganisationContext,
OrganisationProvider
} from "./contexts/OrganisationContext";
export default function App() {
return (
<OrganisationProvider>
<OrganisationContext.Consumer>
{({ organisations }) =>
organisations ? (
organisations.map(organisation => {
return <li key={organisation.id}>{organisation.name}</li>;
})
) : (
<div>loading</div>
)
}
</OrganisationContext.Consumer>
</OrganisationProvider>
);
}
And the updated useEffect in OrganisationsContext.tsx:
React.useEffect(() => {
setOrganisations([
{ id: "1", name: "google" },
{ id: "2", name: "stackoverflow" }
]);
}, []);

Implement useSelector equivalent for React Context?

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.

Resources