Redux-toolkit: Multiple instances of features/slice (using createSlice)

Created on 2 Sep 2020  路  22Comments  路  Source: reduxjs/redux-toolkit

Hi guys,

I would like to ask if it is possible to create multiple instances, e.g. a counter using the createSlice function?
I prepared Sandbox Online, where I included the code from CRA --template Redux.
Link: https://codesandbox.io/s/confident-gould-2tr1b

We have one feature Counter here, which has its name ("counter') and the reducer has been placed under the name of counter in the store.

What if I want to use exactly the same counter twice?
I would like to somehow separate these two counters from each other so that they are independent and use their piece of store.
I tried creating some prefixes and creating a slice with a dynamic name, but there is a problem with selectors and thunks.

<Counter name="one"/>
<Counter name="two"/>

Is there any way to create reusable features in some way using RTK?

Any help is appreciated, have a good day.

All 22 comments

Well, you'll face the problems you are describing, there's not really a way around that. Of course you can use a function to create multiple copies of the same selectors/etc, but there's not much where RTK can help you.

Oftentimes it might also make sense to reevaluate where you are storing the data. If it really is bound to component instances, redux might be the wrong place to store the data in the first place - local component state might be the way to go instead.

So there is no option to do that, even in pure React-Redux without RTK?

As I said, you can always create higher order functions that return different copies of your reducers/selectors/thunks. But there's no "recipe" for that, you'd have to do that to your requirements.

Also, redux is a global state. When you have the same thing multiple times in there, that is often an indicator that you are trying to recreate your component tree as a global state - and that's not really the purpose of it.
It's fine having some state just in the components.
https://redux.js.org/faq/organizing-state#do-i-have-to-put-all-my-state-into-redux-should-i-ever-use-reacts-setstate

Ok, I understand.
So maybe you have a better idea how to implement such functionality:
We have eg. Incidents Page and Users Page and on both page we have exactly the same features. The only difference is entity name so I wanted to create a component . And this will include:

  1. Call API and get all available views for entity eg. User
  2. AppUser select from list one view eg. All active users
  3. Call API and fetch data for selected view and display table with fetched data
  4. AppUser can change columns, sort, add search filters, save view, create new view etc.

So I need to put the data to Redux because I have a lot of features which are interacting with view data and metadata. Moreover, I have setup RealTime using SignalR, so when one AppUser edit some record then another AppUser will se changes immidiately

Hm. I'd go the higher order function route to create slices, selectors etc. for each of these pages then. Google "higher order function redux", that should give you plenty of resources on how to do such things. Of course it will work with RTK's createSlice as well, but there's nothing RTK-specific in there.

Hm. I'd go the higher order function route to create slices, selectors etc. for each of these pages then. Google "higher order function redux", that should give you plenty of resources on how to do such things. Of course it will work with RTK's createSlice as well, but there's nothing RTK-specific in there.

Thank you very much for your answer and suggestions. As far as I understand how to create such a function in classic redux, but I am not quite sure how to create HOC for createSlice and selectors. I'll try to figure something out :)

Also see https://redux.js.org/recipes/structuring-reducers/reusing-reducer-logic .

Thank you! The described mechanism for Redux without RTK is understandable to me, but as I wrote above, I do not know yet how to create such a mechanism using RTK. I'll try to come up with something even on the example of a simple Counter, unless there is an example somewhere?

Yeah, we've got a TS-based example in the docs here:

https://redux-toolkit.js.org/usage/usage-with-typescript#wrapping-createslice

You can see it's the same pattern as shown in that "Reusing Logic" docs page: a function that takes some kind of a "name" argument or similar, and using that to help customize the output.

@markerikson @phryneas I did something like this, but I don't know if this is the way to go. What do you think about it?
https://codesandbox.io/s/confident-gould-2tr1b

That would create dozens of your testThunk and selectors for the same name.

Move all that into your createGenericSlice, generate them once in there and return them from there.

function createGenericSlice(sliceName) {

    const thunk = createAsyncThunk(`${sliceName}/testThunk`, async (payload) => {
        await test();
        return payload;
    });

    const slice = createSlice({
        name: sliceName,
        reducers: {
            // ...
        },
        extraReducers: {
          [testThunk.pending]: (state) => {
            console.log("test thunk pending");
          },
          // ...
        }
    })


    selectCount = (state) => state[counterName].value;

    return {
        thunk, slice, reducer: slice.reducer, actions: slice.actions, selectCount
    }
}

const slice1 = createGenericSlice("1")
const slice2 = createGenericSlice("2")

// usage:
dispatch(slice1.thunk())
useSelector(slice1.selectCount)

@phryneas I agree that your solution is much more optimal, but then how is the Counter component supposed to use it?

You could still put the results in a map object and use actions & selectors keyed by that object map.

Or you go a completely different route, actually put all data in just one slice, put a counterId in your payload or meta and handle all the data in one slice, keyed by that counterId. That's always the problem with these simple examples: I wouldn't do what you are looking for with a counter example at all, but I don't know how far you want to take it.

@phryneas Thank you! This is the solution that solves my problem at the moment. I made one of feature into this approach and can use a given feature on two pages without state clearing on component unmount/mount etc.

I have one more question. If my component is now getting an instance from the cache, what is the best mechanism for storing it?
UseRef - but I have to add .current every time, but it gives me the advantage that I don't have to add this variable to dependency array useEffect, useCallback etc.
UseState / UseMemo - I need to add the variable to dependency array or write // eslint-disable-next-line

You should definitely not store that in your component ever!

Hmm, so how is my Counter component supposed to know which slice instance it should use?

// counterSlice.js
const counterInstances = { 
    counter_A: createGenericSlice('counter_A'),
    counter_B: createGenericSlice('counter_B')
};

// Counter.js
const Counter = (name) => {
    // how to get appropriate instance?

    // eg.
    useEffect(() => {
        dispatch(myCounter.increment());
    }, []);
}

By just referencing it. There's no need to ever save that long-term into the component. It will be the same amount of code, or even more.

// counterSlice.js
const counterInstances = { 
    counter_A: createGenericSlice('counter_A'),
    counter_B: createGenericSlice('counter_B')
};

// Counter.js
const Counter = (name) => {
       const myCounter = counterInstances[name]

    // eg.
    useEffect(() => {
        dispatch(myCounter.increment());
    }, []);
}

Yes, but there is a little problem with such usage. Using JavaScript not TypeScript I lose whole Intellisense in Vscode, but when I create array with counter instances and find appropriate instance
const myCounter = instances.find(x => x.name === 'counterA') I have Intellisense in VSC, but without saving reference on every re-render find method is invoking

You can do TypeScript type annotations in JavaScript and VSCode will recognize them

/** @type {{ [key: string]: ReturnType<typeof createGenericSlice> }} */
const counterInstances = { 
    counter_A: createGenericSlice('counter_A'),
    counter_B: createGenericSlice('counter_B')
};

@phryneas Thank you, you helped me a lot! You are awesome, it works!

Closing this one :)

Was this page helpful?
0 / 5 - 0 ratings