Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Load initialState dynamically with createSlice in Redux Toolkit

Is there a well-known pattern for injecting a payload of dynamic initial state into Redux-Toolkit's initialState object?

That is, I would like to do this -

import initialState from './initialState';

function generateSlice(payload = {}){
  const postsSlice = createSlice({
    name: 'posts',
    initialState: {...initialState, ...payload}, /// inject data here
    reducers: {...}
  })
}

For example, {availableRooms: []} is an empty array, unless injected on init with data {availableRooms: [{...}]}

This pattern doesn't work, however, b/c I want to export actions to be dispatch-able, something like this-

 const postsSlice = createSlice({
    name: 'posts',
    initialState: {...initialState, ...payload},
    reducers: {...}
  })

export {actionName} from postsSlice.actions;

*****
import {actionName} from '../mySlice'

...
const dispatch = useDispatch();
dispatch(actionName('exampleVal'));
...

I am constrained by the airbnb linting rules, so I can't export on let -

let actions; ///Bad
function generateSlice(payload){
  const postsSlice = createSlice({
    name: 'posts',
    initialState: {...initialState, ...payload},
    reducers: {...}
  })
  actions = postsSlict.actions
}

export actions;

The functionality that I am after is a bit easier without using createSlice. The reason for my question is that I have seen in multiple places that createSlice is recommended over createAction + createReducer, but I don't see any simple way to introduce the dynamic data that I am looking for.

I don't know anything about redux-orm but I think the functionality that I am after is similar to this SO question

like image 472
nrako Avatar asked Nov 06 '22 08:11

nrako


2 Answers

Here's my current work-around, which skips createSlice altogether.

In the root render

...
const store = initStore(data);
 <Provider store={store}>
  <App />
</Provider>

And the init function (pared down for brevity)

import {
  configureStore,
  getDefaultMiddleware,
  combineReducers,
} from '@reduxjs/toolkit';
import reservationReducer from '@reservation/reducer';
import spaceHierarchyReducer from '@map/reducer';
import appStoreReducer from '@app/reducer';

let ReduxStore;

function initStore(
  ssrState: Partial<RootStore> = {},
) {
  if (ReduxStore) {
    return ReduxStore;
  }
  const slices = {
    reservation: reservationReducer,
    spaceHierarchy: spaceHierarchyReducer,
    appStore: appStoreReducer,
  };
  const reducer = combineReducers(slices);
  const preloadedState = getInitialState(ssrState);
  const store = configureStore({
    reducer,
    middleware,
    preloadedState,
  });

  ReduxStore = store;
  initDispatch(store);
  return store;
}

In getInitialState, I parse the URL and set-up the store based on business requirements, a mixture of server-side data + url-injectable params. Then, in initDispatch, I invoke store.dispatch() for some init logic based that injected initial state.

Here the usage of Typescript is quite helpful, as it enforces the shape of the data returned from getInitialState as well as the shape of the reducers.

like image 90
nrako Avatar answered Nov 15 '22 07:11

nrako


I found a work around with Redux Tool Kit. I'm kind of new to Redux because Context API cannot rerender React Native Navigation Screens as they are not part of the main tree. I don't know if my approach is good enough, but here was my thinking:

  • generateSlice() wouldn't fill actions variable because at the time the export is made to be used by RTK module, generateSlice hasn't been called yet.
  • At the beginning, RTK module just need the structure and configuration for createSlice, but not the store object yet. Only the configureStore really care about the store itself. So that with a duplicate call: exporting actions with normal default initialState and then recalling it inside generateSlice(initValue) with the real default initialValue seems to work well.

To keep it simpler for everyone, I'm giving an example with the official short tutorial on RTK https://redux-toolkit.js.org/tutorials/quick-start :

counterSlice.js :

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  value: 0,
};

const slicer = initState =>
  createSlice({
    name: 'counter',
    initialState: initState,
    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 += 1;
      },
      decrement: state => {
        state.value -= 1;
      },
      incrementByAmount: (state, action) => {
        state.value += action.payload;
      },
    },
  });

const generateSlice = initState => {
  return slicer(initState).reducer;
};

export const counter = slicer(initialState);

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counter.actions;

export default generateSlice;

store.js becoming a function now, rename it getStore if necessary :

import { configureStore } from '@reduxjs/toolkit';
import generateCounterReducer from '../states/reducers/counter';

export const store = states => {
  return configureStore({
    reducer: {
      counter: generateCounterReducer(states.counter),
    },
  });
};

App.js or index.js where you put the redux Provider:

<Provider
  store={store({
    counter: { value: 7 },
  })}
>

And when I load the component, the value 7 is rendered by default. The only problem with it is that it executes the createSlice 2 times. But since this only happens at the App start, then I see no performance issue with that approach. Maybe the pattern will conflict with advanced usage, so if anyone see any bottleneck, we can discuss it and figure out how to improve it.

like image 44
KeitelDOG Avatar answered Nov 15 '22 06:11

KeitelDOG