-
-
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
Feature discussion: createSlice
behavior
#91
Comments
So I think I'm a little unclear on what you want selectors to do here. Right now it seems like a slice always has one selector, and it's in the form of For "validation", i was imagining you'd create a wrapper for function combineReducers(reducersOrSlices) {
const reducers = {}
for (const [key, reducerOrSlice] of Object.entries(reducersOrSlices)) {
if (reducerOrSlice.slice && reducerOrSlice.slice !== key) { throw new Error() }
reducers[key] = reducerOrSlice.reducer || reducerOrSlice
}
return redux.combineReducers(reducers)
} If you wanted slices to be nestable, you could have it such that giving a slice an id like |
@modernserf : the primary reason for the current behavior is that that's what @neurosnap implemented for https://github.com/neurosnap/robodux , and our implementation is a literal port of what is in As I said in the other thread, I think |
@markerikson is correct. The intention was to eventually add more features to the Overall I'm not happy with |
@neurosnap I eventually figured out the typing for generating additional selectors for all the keys of |
Also I would like to propose that we change the default selector to have a fixed name instead of the current
const { selectors: { getSlice: selectCounter } } = createSlice({
slice: "counter",
//...
}) |
@Dudeonyx : agreed. Also, I'd prefer to use |
Here's a suggestion I posted earlier but was closed in favor of this thread. A possible improvement would be for slices to contain references to other slices. I was thinking something like this:
const rootSlice= createSlice({
references: [slice1, slice2] // Maybe this or something similar
})
// slice1.js
const sliceOne = createSlice({
// initialState, reducers etc.
references: [slice3] // Refers to another slice
}) Then if there is an ergonomic way to access slices within slices you can effectively having implemented a namespace. |
@Ikuoch How do you imagine the referenced slices being used? |
@Dudeonyx In what context did you mean? The way I was thinking is that that ideally for an app, you could create a rootSlice and register all your slices with it. With slices, I think it eliminates the need to have your actions, reducers etc. split across files so that you can have them all in one data structure. Then in your components, you only need to call the single source of truth, the rootSlice. I haven't worked out how it would look like or work yet but maybe you can call something like or... there could be some magic where if you pass in a string to |
Fair enough. I will then refrain from the
Given these points, I propose we create and communicate a razor-sharp definition for what a slice is or is not, and ensure that we don't grow its scope past that definition. Personally, I think we should stay with what we have currently, meaning that a slice is
I see no compelling reason to allow the definition of additional selectors as part of I also don't think it is a good idea to automatically create selectors for attributes of a slice state object. Like auto-generating getters in Java IDE's, it encourages the mindless direct exposition of all state pieces without considering what data is needed by the view layer, and in which form. Based on the slice definition above, the following are changes we could do to the current function:
[*] As an aside, it should be also noted that slices interact somewhat awkwardly with the Redux "ducks"-style module pattern. As a module author, you have the choice of directly exporting a slice plus any non-slice entities like thunk action creators, making the user guess when to write: import module from './module'
module.actions.someAction() and when they need to write instead: import { someThunkAction } from './module'
someThunkAction() Or you can hide the slice from the user by making all slice pieces top-level exports, like this: const slice = createSlice({
// ...
})
export const select = slice.selectors.getSlice
export const someAction = slice.actions.someAction
// ...
export default slice.reducer but then you lose the conciseness you used This dilemma is not a problem as such - maybe |
Another aspect we should consider is composability. As currently defined, slices are not composable at all. Most obviously, the generated slice selector has a hardcoded assumption on where the slice state is placed - namely, under an attribute named after the slice in the root state object. Install the slice reducer anywhere else, and the slice selector breaks. More subtly, composing slices into bigger slices is not really possible because their means of composition "above" and "below" are asymmetrical. A slice reducer assumes to own a state object attribute (the one defined by the slice name), whereas the sub-reducers of a slice own an action type instead. This is a fundamental mismatch that is hard, or perhaps even impossible, to overcome. But there are more problems. Try to imagine a const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
add: (state, { payload }) => state + payload
}
})
const listSlice = createSlice({
name: 'list',
initialState: [],
reducers: {
add: (state, { payload }) => state.concat([payload])
}
})
const slice = combineSlices(counterSlice, listSlice) How would Now, there are two possible courses of action. One is to rethink slices so that they become composable, which boils down to putting fewer assumptions into them about how they are embedded (e.g., by removing the auto-generated slice selector). The other is to acknowledge that slices offer no or only limited composability - but still offer a lot of convenience for the cases they were designed for - and clearly document the limitations. |
Not sure I understand, but it sounds like you want to just import one root slice and be able to reference all other slices through it, vs. importing specific slices the components (containers) want to work with. If so, I don't really like the idea - it feels too "magical" for me, and probably typing it in TS would be a mess again. |
Let me describe how I have been using slices with success in my projects. There are a couple of key ideas that I think are best practices for using redux that avoids some of the arguments against slices.
// packages/messages/index.js
const messages = createSlice({
initialState: {},
reducers: {
add: (state, action) => { ...state, action.payload },
},
slice: 'messages',
});
const messageIdSelected = createSlice({
initialState: '',
reducers: {
set: (state, action) => action.payload,
},
slice: 'messageIdSelected',
});
const actions = {
...messages.actions,
...messageIdSelected.actions
};
const reducers = {
[messages.slice]: messages.reducer,
[messageIdSelected.slice]: messageIdSelected.reducer,
};
const getMessageByIdSelected = (state) => {
const msgs = messages.selector(state);
const messageId = messageIdSelected.selector(state);
return msgs[messageId];
}
const selectors = {
getMessages: messages.selector,
getMessageIdSelected: messageIdSelected.selector,
getMessageByIdSelected,
};
function fetchMessages() {
return (dispatch) => {
fetch('/messages')
.then((resp) => resp.json)
.then((body) => {
dispatch(actions.addMessages(body));
dispatch(actions.setMessageIdSelected(body[0].id));
})
}
}
const effects = {
fetchMessages,
}
export { actions, reducers, selectors, effects }; This is how I create packages and I think it works really well. I'm able to have multiple slices, I can define whatever other selectors, actions I want. I can even create my side effect functions in this file.
I actually like that actions are owned by reducers. When I create a slice no other action is allowed to interact with that reducer. This guarantees that other actions don't slip into the reducer. I think this could be a decision by design.
I think I agree with you here. I currently do not even use In defense of I'm able to get the redux side of things created very quickly and spent most of my time in my side-effects. Overall I think that |
With this approach, the logout side effect needs to have knowledge about all slices affected by the logout, which creates pretty tight coupling. In practice, this can mean that the logout module knows about pretty much every reducer in the system, which makes it an undesirably tight coupling point. I think we should rather encourage developers to embrace the publish-subscribe nature of Redux and let those slices react to the single logout action that need it; it allows you to introduce a new slice that reacts to logout with fewer changes to the existing Redux modules.
I agree, and we should make this as clear as possible. |
It's either that or every other module needs to be dependent on |
I've seen arguments both ways ("multiple slices responding" vs "all the logic in one place"). FWIW, Dan has always said that "multiple slices responding" was the intended usage, and it's something I intend to promote more in the upcoming docs revamp. Also, while you're always free to organize your reducers and actions however you want conceptually, I personally prefer to put as much logic into the reducers as possible. Busy at work atm, but one other quick thought: as far as I can tell, import { createSlice } from "redux-starter-kit";
const slice = createSlice({
initialState: 0,
reducers: {
increment: (state, action) => state + 1,
decrement: (state, action) => state - 1,
},
});
export const { increment, decrement } = slice.actions;
export default slice.reducer; Default-exports a reducer, named-exports action creators. It, uh, quacks like a duck to me :) |
And personally I would go with the other one. It's up to the reducer to know that it should support resetting itself in response to the logout action, something that the "extra reducers" mechanism would solve (or just merging it all into one |
@markerikson, on the "ducks" point: sure, I mentioned that approach in my earlier comment. You improved on it with the destructuring export (👍), but the point stands that you still have to repeat the actions, which weakens the boilerplate-saving benefit. Anyway, this and the logout action discussion are moving off-topic a bit. Of the points I raised, I think others deserve more focus:
|
Swinging back around to this discussion. Thoughts:
So, at this point I'm set on removing it in the next minor release (yay pre-1.0 semver!). Steering the discussion in another direction, I'm combing through the various issues and coming up with this list of other potential changes:
I am already planning to remove the slice selector. I am mostly good with changing Any further thoughts or suggestions on these other items, especially the async/effect related stuff? |
An interesting prior art here that I've enjoyed using is redux promise middleware. So something like this: (with some of the #109 ideas included) const todos = createSlice({
name: 'todos'
initialState: [],
reducers: {
// this probably isn't actually correct but 🤷♂️
setTodos: (_, { payload }) => payload,
addTodo: (state, { payload: newTodo }) => state.push(newTodo),
markCompleted: (state, { payload: index }) => state[index].completed = true,
},
effects: {
// Dispatch and actions would probably be, from what I've seen of other state frameworks like Vuex, basically impossible to Type.
syncTodos: async (state, action, { dispatch, actions }) => {
const newTodos = await someSyncFunc(state, action.payload);
dispatch(actions.setTodos(newTodos));
}
},
});
const ui = createSlice({
name: 'ui',
state: {
loadingTodos: true,
error: null
},
extraReducers: {
// "todos/syncTodos/completed"
[todos.actions.syncTodos.completed]: (state, { payload })=> {
state.loadingTodos = false;
if (payload.error) {
state.error = payload.error;
}
}
}
}); Contrived, but does the general idea make sense? |
I don't know. For me, just using an empty function as a selector always worked pretty well. I don't think adding the complexity of another option would be worth to sometimes omit an empty function. (Although I get that some people are crazy about saving every last character).
We already have the extended way of writing a reducer:
What about extending that?
|
Meh. I'm not sure how that's really any better than:
and just adds more complexity. I'm inclined to punt on that idea for now. |
As someone who has minimal influence on the overall API of To me, async actions can be just as easily created outside of Other apps already do this so one could argue we are slowly converging on an API: However, I feel like if we want to go down that route then we should build on top of I think the other suggestions for improvements make sense. |
Yeah, I've been glancing over at Rematch, Kea, and Easy-Peasy for points of comparison, and there's definitely similarities in how they handle declaring effects of some kind. I'm not sold either way. I can see a point to having a place to declare thunks and listener callbacks. I can see a point to keeping |
I was this close to having That said, I do plan to include the generated reducers in the return object. |
Is there / should there be a way to read the root state inside a slice? |
@jamiewinder : no. See this Redux FAQ entry: https://redux.js.org/faq/reducers#how-do-i-share-state-between-two-reducers-do-i-have-to-use-combinereducers |
I think at this point the major design questions around I'm still open to discussing the "side effects" aspect down the road, but that can be a separate point of discussion (per #76 , etc). |
Co-authored-by: Aaron Pettengill <[email protected]> Co-authored-by: Lenz Weber <[email protected]>
There's been some scattered discussion of how
createSlice
should actually behave, so I wanted to move this into its own thread.I'll keep the initial thoughts short and open things up for discussion:
createSlice
to be included in some form. We're not removing it.createSlice
was inspired by https://github.com/ericelliott/autodux , we should review what functionalityautodux
actually has for point of comparison. (Also lots of similar options in https://github.com/markerikson/redux-ecosystem-links, particularly under "Action / Reducer Generators".)Tagging: @modernserf @denisw @Dudeonyx @BTMPL @lkuoch @mattkahl @doxick
The text was updated successfully, but these errors were encountered: