-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Suggestion: add syntax or examples for nested slices #259
Comments
Can you clarify what you mean by "nested slices"? What do you want the actual state to look like? |
Basically I want to have pieces of state in my slice that are managed by other slices. In the same sense that the root reducer is compromised by several reducer functions combined, I want to create a slice that is compromised by several slices combined. It think something like this is already achievable using nested calls to In my example above, if {
someData: 'hello',
someComplexData: { foo: 'bar', john: 'doe' }
} |
Nested use of |
I see, thanks for your quick response. I still think it's a shame some If using nested |
Yeah, not sure where that mention should go exactly, but please feel free to file a PR that adds that note. |
I had a similar use-case, and here is how I solved it combining the power of Some background: I have a sub-state called "associatedItems". It includes some entities (units, categories, items and formulas) that should be loaded in parallel. So in the import { Action, createSlice, combineReducers } from '@reduxjs/toolkit';
import reduceReducers from 'reduce-reducers';
import { AppThunk } from 'store/store.d';
import { UnitsState, reducer as units, actions as unitsActions } from 'store/entities/units';
import { CategoriesState, reducer as categories, actions as categoriesActions } from 'store/entities/categories';
import { ItemsState, reducer as items, actions as itemsActions } from 'store/entities/items';
import { FormulasState, reducer as formulas, actions as formulasActions } from 'store/entities/formulas';
import selectors from './selectors';
// combined associatedItems state
export interface AssociatedItemsState {
loading: boolean;
loaded: boolean;
units: UnitsState;
categories: CategoriesState;
items: ItemsState;
formulas: FormulasState;
}
const { reducer: originalReducer, actions: originalActions } = createSlice({
name: 'associatedItems',
initialState: {
loading: false,
loaded: false
} as AssociatedItemsState,
reducers: {
startLoading(state) {
state.loading = true;
state.loaded = false;
},
finishLoading(state) {
state.loading = false;
state.loaded = true;
}
}
});
// reducer for the nested entities (aka "associated items")
const nestedEntitiesReducer = combineReducers({
loading: s => s,
loaded: s => s,
items,
units,
categories,
formulas
});
// combine original reducer and nested reducers
const reducer = reduceReducers<AssociatedItemsState>(originalReducer, nestedEntitiesReducer);
// extend actions with a thunk
const actions = {
...originalActions,
loadAssociatedItems: (): AppThunk => async dispatch => {
dispatch(originalActions.startLoading());
await Promise.all([
// each dispatch here actually returns a Promise which always fulfills, no need to catch rejections
dispatch(itemsActions.read()),
dispatch(categoriesActions.read()),
dispatch(unitsActions.read()),
dispatch(formulasActions.read())
]);
dispatch(originalActions.finishLoading());
}
};
// export them all
export {
reducer,
actions,
selectors
}; It is almost the same as combining nested reducers with I could go the other way and use separate nested reducer for UPDATED: used // combine original reducer and nested reducers
const reducer = (state: AssociatedItemsState, action: Action) => ({
...state,
...originalReducer(state, action),
...nestedEntitiesReducer(state, action)
}); |
@agrinko : the Instead, consider using https://github.com/redux-utilities/reduce-reducers , per https://redux.js.org/recipes/structuring-reducers/beyond-combinereducers#sharing-data-between-slice-reducers . |
@markerikson thank you, I didn't know that. Updated my code above using |
Yeah, We've had requests to remove that warning, but opted not to do so. Note that there's a million re-implementations of |
What's the proper way to deal with individual fields? I'm trying to convert an existing project (that does not use redux) that has a state structure that looks like this: {
project: {
id,
collections,
tables,
document
},
user: {}
} The only individual field in that is I'm attempting to do the following for defining the import { createSlice, combineReducers } from '@reduxjs/toolkit'
import reduceReducers from 'reduce-reducers'
import collectionsReducer from './collections.slice'
import tablesReducer from './tables.slice'
import documentsReducer from './documents.slice'
const projectSlice = createSlice({
name: 'project',
initialState: {
id: null
},
reducers: {
setProjectId(state, action) {
state.id = action.payload.id
return state
}
}
})
const nestedEntitiesReducer = combineReducers({
id: // not sure what goes here for 'id',
collections: collectionsReducer,
tables: tablesReducer,
document: documentsReducer
})
const reducer = reduceReducers(
projectSlice.reducer,
nestedEntitiesReducer
)
export const {
setProjectId
} = projectSlice.actions
export default reducer |
I figured it out after looking at @agrinko 's code a few times:
|
reduceReducers will return a flat reducer that will use shared state for each provided reducer, but keep in mind about initialState, since each slice has own initialState. |
I made a import { combineReducers } from "@reduxjs/toolkit";
import reduceReducers from "reduce-reducers";
/**
* Combines a slice with any number of child slices.
*
* This solution is inspired by [this GitHub
* comment](https://github.com/reduxjs/redux-toolkit/issues/259#issuecomment-604496169).
*
* @param sliceReducer - The reducer of the parent slice. For example, if your
* slice is called `mySlice`, you should pass in `mySlice.reducer`.
* @param {object} sliceInitialState - The initial state object passed to
* `createSlice`. This is needed so that we know what keys are in the initial
* state.
* @param {object} childSliceReducers - An object of child slice reducers, keyed
* by the name you want them to have in the Redux state namespace. For example,
* if you've imported child slices called `childSlice1` and `childSlice2`, you
* should pass in this argument as `{ childSlice1: childSlice1.reducer,
* childSlice2: childSlice2.reducer }`. NOTE: The name of each child slice
* should reflect its place in the namespace hierarchy to ensure that action
* names are properly namespaced. For example, the `name` property of the
* `childSlice1` slice should be `mySlice/childSlice1`, not just `childSlice1`.
*/
const combineSlices = (sliceReducer, sliceInitialState, childSliceReducers) => {
const noopReducersFromInitialState = Object.keys(sliceInitialState).reduce(
(prev, curr) => {
return {
...prev,
[curr]: (s = null) => s,
};
},
{}
);
const childReducers = combineReducers({
...childSliceReducers,
...noopReducersFromInitialState,
});
return reduceReducers(sliceReducer, childReducers);
};
export default combineSlices; Note that you need to pass it the initial state from the parent slice so that it knows what keys to make dummy no-op reducers for. Thus, here's an example of how it could be used: import mySlice2 from "./mySlice2"
import mySlice3 from "./mySlice3"
import combineSlices from "./combineSlices"
// Note that `initialState` is defined as a variable before being passed
// to `createSlice` since it will need to also be passed to `combineSlices`.
const initialState = {
count: 0,
}
const mySlice = createSlice({
name: "mySlice",
initialState,
reducers: {
setCount(state, action) {
state.count = action.payload
},
}
})
export default combineSlices(mySlice.reducer, initialState, {
mySlice2: mySlice2.reducer,
mySlice3: mySlice3.reducer,
}) |
After defining combined slices in this way ^ (RE: @agrinko and @jessepinho), how do you import them and add them to your store? Any examples of defining selectors (for "nested" state) and passing them to Is the discussion above outlining a pattern that is supported by the Redux devs or is there another way to approach updating sub-properties of state that allows one to use separate reducers/selectors for the parent state object and child state objects? |
Here is gist I extracted from my project which allows to create nested slices - https://gist.github.com/HemantNegi/27bf13b9f7ded5ae420f21932b082ee8 NOTE: nesting is not recommended in general as there are some problems associated with it. eg. The state diff calculation will be slow, no access to parent slice state from child slice (can be overcomed with a custom middleware) P.S. I did not used this approach, instead normalised my state shape as described here - https://redux.js.org/usage/structuring-reducers/normalizing-state-shape |
There might actually be a better pattern available now. I suggested this to someone the other day: // however you are making this reducer - createReducer, createSlice, combineReducers, etc
const additionalReducer = createReducer()
const mySlice = createSlice({
name: "mySlice",
initialState, // however you need to make this,
reducers: {
// whatever case reducers here
},
extraReducers: (builder) => {
builder.addMatcher(
// _always_ give this reducer a chance to run too
() => true,
additionalReducer
);
}
}) |
Currently
redux-toolkit
does not provide (or I couldn't find) an integrated way to define nested slices/reducers.To split some slice logic into smaller slices, I'm currently doing something like this:
The above example works, but it's a lot of boilerplate code, and I can't shake the felling there should be a better way to describe nested slices.
The text was updated successfully, but these errors were encountered: