How do I use Redux Toolkit with multiple React App instances? - reactjs

We have written a React app using Redux via Redux Toolkit. So far so fine.
Now the React app shall be rendered into multiple different elements (each element shall get a new app instance) on the same page.
The rendering part is straight forward: We just call ReactDOM.render(...) for each element.
The Redux part again brings some headache.
To create a new Redux store instance for each app instance, we call the configureStore function for each React app instance. Our slices look similiar to this:
import { createSlice } from '#reduxjs/toolkit'
import type { RootState } from '../../app/store'
// Define a type for the slice state
interface CounterState {
value: number
}
// Define the initial state using that type
const initialState: CounterState = {
value: 0,
}
const counterSlice = createSlice({
name: 'counter',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
}
},
});
export const increment = (): AppThunk => async (
dispatch: AppDispatch
) => {
dispatch(indicatorsOrTopicsSlice.actions.increment());
};
export const decrement = (): AppThunk => async (
dispatch: AppDispatch
) => {
dispatch(indicatorsOrTopicsSlice.actions.decrement());
};
// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState) => state.counter.value
export default counterSlice.reducer
Please note, that currently we create and export each slice statically and only once. Here comes my first question: Is this actually valid when creating multiple store instances or do we actually need to create also new slice instances for each app/store instance?
For the simple counter example provided, doing not so, seems to work, but as soon as we use an AsyncThunk as in the example below the whole thing breaks.
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], isLoading: false, hasErrors: false },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
builder.addCase(fetchUserById.pending, (state, action) => {
state.isLoading = true;
});
builder.addCase(fetchUserById.rejected, (state, action) => {
state.isLoading = false;
state.hasErrors = true;
});
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload);
state.isLoading = false;
state.hasErrors = true;
});
},
});
I believe the breaking starts here because of interdifferences between the events fired from dispatching the AsyncThunk.
Thereby I think the solution is to call the createAsyncThunk function for each app/store/slice instance. Are there any best practices for doing so? Of course this breaks the beauty and functionality of static exports and requires kind of a mapping, hence I'm asking.

My original suspicion that the AsyncThunk-part was responsible for the interferences between the stores of the different React app instances was wrong.
The source was something different not visible in the examples provided in my question.
We use memoized selectors via createSelector from reselect. Those were created and exported like the rest statically which in fact is a problem when working with multiple store/app instances. This way all instances use the same memoized selector which again doesn't work correctly thereby, since in the worst scenario the stored values of the dependency selectors are coming from the use from another store/app instance. This again can lead to endless rerenderings and recomputations.
The solution I came up with, is to create the memoized selectors for each app instance freshly. Therefore I generate a unique id for each app instance which is stored permanently in the related Redux store. When creating the store for an app instance I create also new memoized selectors instances and store them in a object which is stored in a static dictionary using the appId as the key.
To use the memoized selectors in our components I wrote a hook which uses React.memo:
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { selectAppId } from "../redux/appIdSlice";
import { getMemoizedSelectors } from "../redux/memoizedSelectors";
// Hook for using created memoized selectors
export const useMemoizedSelectors = () => {
const appId = useSelector(selectAppId);
const allMemoizedSelectors = useMemo(() => {
return getMemoizedSelectors(appId);
}, [appId]);
return allMemoizedSelectors;
};
Then the selectors can be used in the components like this:
function MyComponent(): ReactElement {
const {
selectOpenTodos,
} = useMemoizedSelectors().todos;
const openTodos = useSelector(selectOpenTodos);
// ...
}
and the related dictionary and lookup process would look like this:
import { createTodosMemoizedSelectors } from "./todosSlice";
/**
* We must create and store memoized selectors for each app instance on its own,
* else they will not work correctly, because memoized value would be used for all instances.
* This dictionary holds for each appId (the key) the related created memoized selectors.
*/
const memoizedSelectors: {
[key: string]: ReturnType<typeof createMemoizedSelectors>;
} = {};
/**
* Calls createMemoizedSelectors for all slices providing
* memoizedSelectors and stores resulting selectors
* structured by slice-name in an object.
* #returns object with freshly created memoized selectors of all slices (providing such selectors)
*/
const createMemoizedSelectors = () => ({
todos: createTodosMemoizedSelectors(),
});
/**
* Creates fresh memoized selectors for given appId.
* #param appId the id of the app the memoized selectors shall be created for
*/
export const initMemoizedSelectors = (appId: string) => {
if (memoizedSelectors[appId]) {
console.warn(
`Created already memoized selectors for given appId: ${appId}`
);
return;
}
memoizedSelectors[appId] = createMemoizedSelectors();
};
/**
* Returns created memoized selectors for given appId.
*/
export const getMemoizedSelectors = (appId: string) => {
return memoizedSelectors[appId];
};

Related

How to solve Redux Toolkit circular dependency action -> selector -> reducer -> action (.fullfilled of undefined)

When defining my actions, reducer, and selectors I wanted to try to keep these in separate files so that my folder structure looks like this:
- store
-- foo.actions.ts
-- foo.reducer.ts
-- foo.selectors.ts
In my foo.actions.ts file I define all of my actions which include AsyncThunk actions.
Some of these actions reference selectors in the foo.selectors.ts file. e.g.
import { selectById } from "./foo.selectors.ts"
export const barAction = createAsyncThunk<IFoo, { foo: IFoo }, { state: IFooState, rejectValue: IFoo }>(
FooActionTypes.Bar,
async (payload: {foo: IFoo}, {getState, rejectWithValue}) => {
const existingFoo = selectById(getState(), payload.foo.id);
...
}
);
The foo.selectors.ts file references the foo.reducer.ts file to use the entityAdapter that is used to create the initial state of the reducer. e.g.
import { fooAdapter } from "./foo.reducer.ts"
export const { selectById } = fooAdapter.getSelectors();
foo.reducer.ts then references foo.actions.ts in the createReducer function to reference the thunk types.
import { barAction } from "./foo.actions.ts"
export const fooAdapter = createEntityAdapter<IFoo>(...);
const initialState = fooAdapter.getInitialState();
export const reducer = createReducer(initialState, builder =>
builder
.addCase(barAction.fulfilled, ...)
):
This creates a circular dependency of actions -> selectors -> reducer -> actions which in turn causes the error Cannot read properties of undefined (reading 'fulfilled')
Is there any way to fix this whilst still maintaining the folder structure or is it unavoidable to have the thunks and reducer in the same file?
Adding a method body to your builder callback could already help.
export const reducer = createReducer(initialState, builder => {
builder
.addCase(barAction.fulfilled, ...)
}
);
Other than that you could move your entityAdapter out into a fourth file.

React Redux Server Side Rendering with code splitting and injecting

Sorry for bad English.
I'm struggling with ssr(pure react) + redux + code splitting(#loadable) + injecting redux. (highly effected by react-boilerplate)
currently my code working great without preload data.
I don't know how can I handle ssr preload data before inject reducer.
here is example to help my problem is.
store = {
global: { // default
key: 'value' // this is done. ssr working great using this value.
},
injected: { // dynamically injected. using replaceReducer per page. (same with react-boilerplate)
key: 'value' // I want to put this value on ssr preload. (not working)
}
}
When it done, it said
Unexpected property "injected" found in previous state received by the reducer. Expected to find one of the known reducer property names instead: "global". Unexpected properties will be ignored.
I know why this error comes(because initial store does not has 'injected' store.), but I don't know How can I fix it properly.
Is there any usage example?
Here is my thought, but it seemed not proper answer.
insert key for preload data on 'global'.
put preload data on 'global' in server.
Move global to injected store(in this case, 'injected') when injecting is done.
voila!
reducerInjector.js
export const injectState = (reducers, preloadedState = {}) =>
Object.keys(reducers).reduce((result, key) => {
const finalReducers = result;
if (typeof reducers[key] === 'function') {
finalReducers[key] = (state = preloadedState[key], action) => reducers[key](state, action);
}
return finalReducers;
}, {});
export const createInitialState = (reducers, preloadedState = {}) =>
Object.keys(preloadedState).reduce((r, key) => {
if (!reducers[key]) return r;
return { ...r, [key]: preloadedState[key] };
}, {});
export const createReducer = (staticReducers, asyncReducers, preloadedState) =>
combineReducers(injectState({
...staticReducers,
...asyncReducers,
}, preloadedState));
export default function reducerInjector(store, staticReducers, preloadedState) {
// Add a dictionary to keep track of the registered async reducers
store.asyncReducers = {};
// Create an inject reducer function
// This function adds the async reducer, and creates a new combined reducer
store.injectReducer = (key, asyncReducer) => {
store.asyncReducers[key] = asyncReducer;
store.replaceReducer(createReducer(staticReducers, store.asyncReducers, preloadedState));
};
return store;
}
...
import reducerInjector, { createReducer, createInitialState } from './reducerInjector';
const configureStore = (initialState, ssr) => {
const sagaMiddleware = createSagaMiddleware({});
const createStoreWithMiddleware = compose(applyMiddleware(thunkMiddleware, sagaMiddleware));
const reducer = createReducer(rootReducer, {}, initialState);
const initialData = createInitialState(rootReducer, initialState);
let store = createStoreWithMiddleware(createStore)(reducer, initialData);
store = reducerInjector(store, rootReducer, initialState);
return store;
}

Action must be plain objects. Use custom middleware for async actions

I was trying make my react-native code prittier and I tried use Actions as new actions in following way:
actions:
import { Action } from "redux";
export const actionType = {
INCREMENT_ACTION: "example/INCREMENT_ACTION",
DECREMENT_ACTION: "example/DECREMENT_ACTION"
};
export class ExampleIncrementAction implements Action {
type = actionType.INCREMENT_ACTION;
}
export class ExampleDecrementAction implements Action {
type = actionType.DECREMENT_ACTION;
}
export type ExampleActions = ExampleIncrementAction | ExampleDecrementAction;
so reducer can looks like that:
export default function exampleReducer(
state = initialState,
action: ExampleActions
) {
switch (action.type) {
...
But now the strange errors appear: When I try use action in following way:
const increment = () => ({ type: 'example/INCREMENT_ACTION' });
const decrement = () => new ExampleDecrementAction();
const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ increment, decrement }, dispatch);
};
calling the increment action is fine, but calling decrement action caused error: Action must be plain objects. Use custom middleware for async actions.
My question: Is there a way to use new class object as action? What I'm doing wrong?
Redux by design only accepts plain objects as actions to avoid various possible mistakes. For example passing a Promise as an action.
Is there a way to use new class object as action?
To use some class instances as actions you could either implement a custom middleware to overcome this limitation (this might lead to errors in future if someone say dispatches a Promise as action)
const useCustomActions = state => next => action => next({ ...action })
Or do it in action creator per action level
const decrement = () => ({...new ExampleDecrementAction()});

How to connect Reused Reducers Logic into React components?

I'm using the pattern described here that show us how to reuse reducer logic for other similar purposes.
So, my reducer code is like the code below:
function ContentFilterReducer(entity = ''){
initialState.groupFilter = entity;
return function ContentFilterReducer(state = initialState, action)
{
// is the entity that we want to update?
if (action.item !== undefined && action.item.groupFilter !== entity)
return state;
switch (action.type) {
case ContentFilterTypes.ADD_ITEM:
return {
// we set the
groupFilter: action.item.groupFilter,
listObjects : state.listObjects.push(new Map({
id: action.item.id,
description: action.item.description,
imgSrc: action.item.imgSrc
}))
}
default:
return state;
}
}
}
My combinedReducer describe a reducer for each purpose, as we can see below:
const SearchReducers = combineReducers({
// contains all allowed filters to be selected
UsersContentFilterReducer : ContentFilterReducer(Types.users),
OrganizationsContentFilterReducer : ContentFilterReducer(Types.organizations)
})
Everything is working great, however I'd like to know, how to connect it in a React component using the connect function from React-Redux?
As we can see, I can define the reducer setting an entity (a simple char like 'a', 'o', etc) and, to call the specific reducer, I need only set the entity in my action. And now, the problem is how to connect a specific reducer for a specific presentational component?
The code below is my HOC container that connect the reducer to a specific component, however, the code is the old version, without defining wich reducer should call.
const mapStateToProps = (state, action) => {
return {
contentList: ContentFilterReducer(state.ContentFilterReducer, action)
}
}
/**
*
* #param {contains the action that will be dispatched} dispatch
*/
const mapDispatchToProps = (dispatch) => {
return {
onAddClick: (groupFilter, filterDescription, operator, value) => {
dispatch(AddFilter(groupFilter, filterDescription, operator, value));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ContentFilterField)
You don't connect a reducer. You connect a component to the Redux store. I won't name my state xxxReducer, it's a little bit confusing.
I'm not sure what your app looks like, for a simple case, you just need to: (connect both state)
const mapStateToProps = (state) => {
return {
userContentList: state.SearchReducers.UsersContentFilterReducer,
organizationContentList: state.SearchReducers.OrganizationsContentFilterReducer,
}
}
If you want to switch between usersContent and organizationsContent dynamically based on your component's state, what you need is a selector function.
This is the official redux example: https://github.com/reactjs/redux/blob/master/examples/shopping-cart/src/reducers/index.js#L10-L26
These functions are selectors, you import and use them to get the state you want.
So you will create something like getContentList and it accepts a type like Types.users
const mapStateToProps = (state) => {
return {
// suppose you save current type saved in SearchReducers.type
contentList: getContentList(state.SearchReducers.type)
}
}
Also, the second parameter of mapStateToProps is ownProps not action.

What are selectors in redux?

I am trying to follow this code in redux-saga
export const getUser = (state, login) => state.entities.users[login]
export const getRepo = (state, fullName) => state.entities.repos[fullName]
Which is then used in the saga like this:
import { getUser } from '../reducers/selectors'
// load user unless it is cached
function* loadUser(login, requiredFields) {
const user = yield select(getUser, login)
if (!user || requiredFields.some(key => !user.hasOwnProperty(key))) {
yield call(fetchUser, login)
}
}
This getUser reducer (is it even a reducer) looks very different from what I would normally expect a reducer to look like.
Can anyone explain what a selector is and how getUser is a reducer and how it fits in with redux-saga?
getUser is not a reducer, it is indeed a selector, that is, a function that knows how to extract a specific piece of data from the store.
Selectors provide an additional layer such that if you altered your store structure and all of a sudden your users were no longer at state.entities.users but instead at state.users.objects.entities (or whatever) then you only need to update the getUser selector and not every place in your app where you were making a reference to the old location.
That makes them particularly handy when it comes to refactoring your Redux store.
Selectors are getters for the redux state. Like getters, selectors encapsulate the structure of the state, and are reusable. Selectors can also compute derived properties.
You can write selectors, such as the ones you saw in redux-saga. For example:
const getUsersNumber = ({ users }) => users.length;
const getUsersIds = ({ users }) => users.map(({ id }) => id);
etc...
You can also use reselect, which is a simple “selector” library for Redux, that memoize selectors to make them more efficient.
Selectors are functions that take Redux state as an argument and return some data to pass to the component.
const getUserData = state => state.user.data;
Why should it be used?
One of the main reasons is to avoid duplicated data in Redux.
Your data object shape keeps varying as your application grows, so rather than making changes in all the related component.It is much recommended/easier to change the data at one place.
Selectors should be near reducers because they operate on the same state. It is easier for data to keep in sync.
Using reselect helps to memoize data meaning when the same input is passed to the function, returns the previous result rather than recalculating again.So, this enhances your application performance.
function mapStateToProps (state) {
return {
user: state.user,
}
}
initialState of reducer by user store
const initialState = {
isAdmin:false,
isAuth:false,
access:[1,2,5]
};
class AppComp extends React.Component{
render(){
const {user: { access:access}} = this.props;
const rand = Math.floor(Math.random()*4000)
return (<div>
{`APP ${rand} `}
<input type="button" defaultValue="change auth" onClick={this.onChangeUserAuth} />
<p>TOTAL STATUS COUNT IS {access.length}</p>
</div>)
}
}}
but you can use selector
var getUser = function(state) {
return state.user
}
const getAuthProp = createSelector(
getUser,
(user) => user.access
);
function mapStateToProps (state) {
return {
// user: state.user,
access: getAuthProp(state)
}
}
Main Problem is this component use all user: state.user and any changes in user (etc isAdmin ,isAuth, access) runs rerender this component which need only part of this store - access!!!
In Redux, whenever an action is called anywhere in the application,
all mounted & connected components call their mapStateToProps
function. This is why Reselect is awesome. It will just return the
memoized result if nothing has changed.
In the real world, you will most likely need the same certain part of
your state object in multiple components.
https://medium.com/#parkerdan/react-reselect-and-redux-b34017f8194c
The createSelector function provided by Reselect implements the most basic way to derive a selector from previous selectors. The simplest use case is to derive a selector from a single other selector. In this case, the parameters to createSelector are the input selector and a function transforming the result of that selector into the result of the new selector. For example
var getProducts = function(state) {
return state.products
}
import {getProducts} from '../app/selectors'
import {createSelector} from 'reselect'
export const getProductTitles = createSelector(
getProducts,
(products) => products.map((product) => product.get('title'))
)
This is equivalent to (ignoring memoization):
import {getProducts} from '../app/selectors'
export const getProductTitles = (state) => {
return getProducts(state).map((product) => product.get('title'))
}
The createSelector function can combine data from multiple selectors as well as from a single selector. We can pass any number of selectors to createSelector, and their results will be passed to the function passed as the final argument. For a (somewhat contrived) example:
const isInCheckout = createSelector(
getIsShippingPage,
getIsBillingPage,
getIsConfirmationPage,
(isShipping, isBilling, isConfirmation) =>
isShipping || isBilling || isConfirmation
)
is equivalent to
const isInCheckout = (state) => {
return (
getIsShippingPage(state) ||
getIsBilingPage(state) ||
getIsConfirmationPage(state)
)
}
common pattern when writing mapStateToProps functions with selectors is to return an object with each key storing the result of a particular selector. The createStructuredSelector helper function in Reselect lets us write this pattern with the minimum of boilerplate. For example, if we writ
const mapStateToProps = createStructuredSelector({
title: getProductTitle,
price: getProductPrice,
image: getProductImage
})
it is equivalent to
const mapStateToProps = (state) => {
return {
title: getProductTitle(state),
price: getProductPrice(state),
image: getProductImage(state)
}
}
https://docs.mobify.com/progressive-web/0.15.0/guides/reselect/

Resources