Closed markerikson closed 5 years ago
Personally I think that redux core being the size it is right now is a good thing, so wouldn't recommend moving RSK into core in any form, nor changing the "scope" of the core.
If you'd go with the scoped package name, I would see things like:
@redux/core
for current redux
@redux/react
for current react-redux
(same pattern for bindings for other libraries)@redux/starter-kit
And then you could also consider moving redux-thunk
into @redux/thunk
as majority of users will use either thunks or sagas. Having the scoped package would also somehow fit into Dan's idea of "small api, recommended 3rd party libs"?
Very quick side tangent: there's still haziness around the @redux
scope / name on NPM. I know we now own @reduxjs
. I think Dan owns the redux
NPM user based on some earlier discussion, but we've had some confused-ish discussions about trying to get me and Tim added to that, and I'm not clear on the nitty-gritty details on trying to publish stuff "org"-style from multiple people when there's a "user" by that name. It hasn't been a high enough priority for me to keep pinging Dan about for further investigation.
Let's leave the scoping/renaming aspect out of this for now. This thread is gonna be bike-sheddy enough without bringing that into it :)
Also, to clarify the point of the discussion:
I'm particularly interested in what other functionality should be included in the redux
package exports list.
For example, two items off the top of my head that would be reasonable to have built-in:
redux-thunk
: the most commonly used middleware, and the starting point for any meaningful non-component logicreduce-reducers
: a small function to sequentially execute reducers, instead of "in parallel" the way combineReducers
worksBoth are small, simple, and unlikely to change much going forward. (Actually, looking at reduce-reducers
, I'm surprised to see it's gone from like 7 lines to about 30 - looks like it accepts an initial state now.)
Ok, if this is more about "what could we add to the core, as option, and not enforced by default", I'd vote for:
redux-thunk
createReducer
function from RSK; it's really handy and I feel like the map notation for reducers is underappreciated - having it as an option (available, documented in the main API docs) would help ease-in people (map of actions vs. switch)configureStore
or alter createStore
in some way tat makes it easier to add the often used tooling (devtools) - I like the fact it accepts object notation with a bool that enables devtools I'm not sold on the createAction helper, IMO it obfuscates the mechanizm a bit, and action creators are already simple enough.
I realize everything I'm saying below has already been said, but I think it's important to summarize why we would do something like this.
Redux at its core (heh) provides a sane way to manage state in (more often than not) React applications. It provides a really great dev experience (once you learn it) that makes your applications very easy to debug.
Learning Curve: Redux has a steep learning curve. More concerning than that (to me), I see a lot of people using it poorly. Returning thunks when they only need to dispatch a single action, using mapDispatchToProps manually, and worst issuing setTimeouts and side effects in reducers.
Boilerplate: Redux requires a lot of boilerplate: laying out switch statements, action creators, selectors etc. This compounds the learning curve problem because once a single dev learns it wrong the other devs copy paste the bad code.
There's a general murmur of "Redux is dead, long live whatever." Graphql libraries handle state management quite efficiently (on the api layer) and React is bringing in a very sane way to manage state with hooks.
Redux is not shiny This is stupid but I think its true. Newer libraries are shinier, "thought leaders" don't talk about why you should use Redux because there's nothing new to talk about, and as a result Redux is starting to look a lot like underscore, jQuery, or any number of libraries that were once popular but now seem like a lot of trouble.
That brings us to the conversation we're having at the moment. Should we do something like make RSK the core library? I think a big drastic move is the kind of thing that solves the Redux is not shiny problem and is a very fair attempt at solving the learning curve and boilerplate problems constantly discussed.
With all of this in mind, these are the types of things I'd think would be interesting moves for a new version of redux. These are in no particular order, and can be considered the ramblings of a lunatic.
At the risk of totally running this conversation off the rails, I'll toss out one more maybe-crazy idea:
What if we were to rewrite the core lib in TypeScript?
edit
Hah, I shoulda read @matthew-gerstman 's comment all the way through first :)
To be fair I buried that in there.
Another idea in my head. Two hooks we can provide
withState(selector: Function)
- mapStateToProps equivalent
withDispatch(actionCreator: Function)
- mapDispatchToProps equivalent.
Also what if the thunk API looked like this
function actionCreator({dispatch, getState}, ...argsItNeeds)
And then when we do the binding we simply bind the first parameter.
I've seen a lot of users do dumb things like reverse these two
(dispatch, getState) => businessLogicFunc()
businessLogicFunc() => (dispatch, getState)
This would alleviate that.
Let's focus on just the Redux side of things, not React Redux. That's better suited for https://github.com/reduxjs/react-redux/issues/1063 .
I will try to keep my idea around Redux, because I see also references to React.
Premise: for me Redux must stay agnostic as much as possible and it shouldn't contain third party libraries (unless they become built in inside the core lib). I introduced Redux inside a SPA that wasn't react and it worked like a charm. Fast, reliable and neat. Teammates loved the order introduced by redux.
Something nice that the core library should provide, are some utilities that are missing at the moment:
withState
: an high order function that, as input needs a function and it returns a function with the current state as last argumentwithDispatch
: an high order function that returns a function with the dispatch
as last argumentWhen working outside the framework, these utilities become really handy.
What if we were to rewrite the core lib in TypeScript?
Other than the benefits to redux
for having typesafe development, what benefit would this provide to users that the existing type definitions do not?
Built in support for thunks (with maybe a less confusing API)
I think bringing redux-think in is a good idea. I'm not sure why people find the existing API confusing and I'm not sure how it could be improved to be less confusing, whilst still being a thunk.
Perhaps the solution is not to bring in thunks bit rather something like redux-promise
?
Simple boolean config for devtools on/off ( default to on)
+1. As someone using the logOnlyInProduction
option, perhaps accepting an options object as well to the devtools
option could make it a bit more flexible?
Make enhancers second class citizens. I think these confuse people more than help them. I'd actually consider moving this out of the core library and into a second package.
I don't think you could (easily) move them out of the core package as a applyMiddleware
is an enhancer. I guess the createStore
function could just accept middleware as params at the end?
const store = createStore(reducer, initialState, thunk, sagaMiddleware, logger /*, ...*/);
Side-effects middleware. Adding another first class middleware that says "this action was dispatched, call this other action creator"
I think this is a great idea. It's probably the biggest reason I've seen people reach out to redux-saga
and redux-observable
and then they get all caught up in the complexities of those more powerful async/side-effect frameworks. Having a simple "listener" style middleware would be fantastic. Not sure what the best API would look like, but something like could work:
const listener = ({ getState, dispatch }, action) => {
if (action.type == 'PING') {
dispatch({ type: 'PONG' });
}
};
applyMiddleware(actionListeners(listener /*, listener2, listener3,...,*/));
It's simultaneously thunk and reducer like, so groking it should not be too difficult for redux users. It could even have a withExtraArgument
variant that appends a third incoming argument to the listener.
Perhaps something like this already exists or this is worth making into a seperate package anyway?
A mergeReducers utility. Essentially combineReducers
, but instead of putting each reducer at on a key, it merges their output. e.g:
const a = (state, action) => ({ c: 0 });
const b = (state, action) => ({ d: 1 });
combineReducers({ a, b });
// { a: { c: 0 }, b: { d: 1 } }
mergeReducers([a, b]);
// { c: 0, d: 1 }
If RSK becomes more official, perhaps the existing combineReducers
(with all the warnings) can move into there and the core library can have a more cut down version?
An addReducer
function (andremoveReducer
?) that allows additional root reducers to be added to the store (merging their output). The dynamic reducer/codesplitting story of redux is currently very messy, partly do to the need to replace the whole reducer rather than just appending more.
Some way of accessing the root state within a combineReducers
slice reducer. I know this has been requested and shot down, like, a billion times now, but there is a reason it is so widely requested. The response is always, "combineReducers
is only one way to construct a reducer, you can always write your own" (or some variant of that), but IMO, many users don't want to write their own (there's a reason it is practically the default way to compose reducers). They want to write small, simple reducer functions and let someone else worry about the complexities of composing them together. I'm not say the API of basic reducer should change (although I can't think of any alternatives), but having some way of accessing it would be fantastic.
I'll think some more and see if I come up with anything else. I'm really excited to see what suggestions others have!
Should we do something like make RSK the core library?
I'd be against; the redux core is self-sufficient and some of the design choices of RSK are just obfuscating things and making things harder to understand/use (createAction
).
Built in support for thunks (with maybe a less confusing API)
Not sure which part of the API people find confusing: the arguments, or the fact that you have to return a function? As for replacing it with redux-promise
- I'd be against this one. Seeing as how we aim to make Redux easier to understand, I would not include another concept (Promises) only for this point. There are still a lot of devs out there that are confused about how to work with Promises.
Rewrite into TS
Yeah, that would benefit the developers, not the community. I'm for it, but might need to consider this could possibly affect contributors.
An addReducer function (andremoveReducer?) that allows additional root reducers to be added to the store (merging their output).
Is there any prior art for this? What API do you see? Something like store.addReducer('sliceName', reducer)
? That would be a good idea actually; might help modularize libraries (e.g. you no longer need to include redux-form
reducer in your createStore
, you just import the store and register it whenever you need; we're doing something like this in our app but thanks to replaceReducers
)
An addReducer function (andremoveReducer?) that allows additional root reducers to be added to the store (merging their output).
Is there any prior art for this?
I don't want to derail the conversation too much with detail as they can be worked out later once we decide what we want to include, if anything, but you asked, so...
Shameless plug: This is more of less what the core package of redux-dynostore
does (except we call it attachReducer
instead of addReducer
).
It would also benefit proposals like reactjs/react-redux#1150.
What API do you see?
As long as the reducers return objects (which it should be to support adding a new key anyway), there's actually no reason you need to define the sliceName
and the API could just be store.addReducer(reducer)
. You just (shallow) merge the result from each reducer into the result of the previous one. Potentially the sliceName
could be an optional second argument that would just run the reducer through combineReducers({ [sliceName]: reducer })
before adding it to the reducers array.
Either defining the sliceName
or merging the results, the definition of the root reducer(s) would probably need to ensure that function return an plain object, which I imagine would make this a rather controversial change for the library. I could argue that most apps already do this thanks to combineReducers
and that it's generally a good idea for readability, debugability and refactorability (and if your app is simple enough to only need a single array or a scalar value, then your probably didn't need redux
in the first place).
As for replacing it with
redux-promise
- I'd be against this one.
TBH, me too. I'm more than comfortable with the thunk API and think you couldn't get a simpler async/side-effect model. I made an assumption from my own very limited experience that most devs are pretty comfortable with promises these days and most uses of thunks use promises internally anyway (i.e. AJAX requests). I guess I missed the mark a bit on that one.
RSK is still far from feature complete and, while it is already a step up, it still has basic issues which have been discussed but are hard to find a solution for. Merging it into core makes more sense for functionality that will be used for a majority of implementations.
E.g. createStore
(core) vs createStore
(RSK) "feels" weird to ship them both with Core; "We know createStore is a bit clunky, so we also give you an easier version to go with it"...
Keeping redux framework/usage agnostic is pretty high up on the list imho.
Will it ship with Immer, even though the majority is not using it? Personally, i like working with Immer, since i know the do's and don'ts of mutability/immutability. I don't think the majority of people do, and this enables them to write wrong code with a safety net. It will greatly reduce the amount of questions about errors related to redux, yet (almost) nobody really knows why it then doesn't work somewhere else.
While it is good for a large amount of usage-cases, it's also very limited in its use and most projects i see it is not used. If it will be merged, why not provide it by default from the store ? The size-impact is minimal.
Providing helper functions, like combineReducers
and the proposed createReducer
makes more sense, since most of them are pretty much hashed out and limited in their usage:
They all make sense since they greatly remove boilerplate, increase readability and provide basic functionality without being opinionated.
createAction
While i use createAction
(or createActions(prefix, actionMap)
) in every project i do, i wouldn't add it, since every implementation doesn't cover the use case people use it for:
(type) => (payload) => ({ type, payload })
(type, identityReducer) => (...args) => ({ type, payload: identityReducer(...args) })
(type, identityReducer) => (...args) => ({ type, ...identityReducer(...args) })
(type) => (payload) => ({
type,
... (payload instanceof Error) ? ({ payload }) : ({ error: payload })
})
Which one is going to be the createAction
to rule them all?
Rewriting it to typescript would indeed improve DX while having no (direct) negative impact on current usage.
Redux (core) in itself is a very simple concept to grasp and to work with. Merging it with RSK moves devs away from understanding how it actually works. Moving towards "provided" utlity libraries with the redux namespace indeed feels like the way to go to me.
While it is good for a large amount of usage-cases, it's also very limited in its use and most projects i see it is not used. If it will be merged, why not provide it by default from the store ? The size-impact is minimal.
Having it active by default would be a breaking change for the API, and mgiht break some assumptions the user already have (maybe there's a middleware thats already using functions as actions for different reasons; it would also make it harder to teach IMO).
Would also be against immer; the docs could talk about it in greater details, in section that would also talk about performance improvements toolset, like reselect.
I'm of the opinion that the core should be as minimal as possible. Move out things like combineReducers
, bindActionCreators
, and compose
into their own libraries (or pull from something like lodash in compose
's case).
I really don't care if Redux has enough "shiny" features. That's not the point. It's a design pattern codified as a library folks can rally around. I would be happy if there was never another release of Redux, as it's very hard to be improved upon without essentially becoming a different library.
I think RSK should stay its own thing. I'm pretty firm on that too. It forms opinions around the library, but there are plenty of other approaches that are equally as valid for certain use cases. We don't need those opinions leaking into the core, making it harder or less clear for how other structure choices can make use of the library.
@timdorr we could leave redux-core really light and do something like redux-tools
that includes all these shiny features we've mentioned on top of redux.
As another note I wanna zero in on the rewriting redux into Typescript. If we were to do that it would give our users a couple of advantages that are worth mentioning.
Improved trust in redux typings. - Redux itself doesn't use these typings and as a result there's less trust. Anecdotally, I've seen a lot of people hand edit the Redux typings (and not PR back to source) because something doesn't line up with what they expect.
Improved editor debugging for TS users - When you start going down the source tree in VSCode (or any intellisense editor) it will lead down to the typings of a function as opposed to the source. Having these side by side allows the user to hunt down the issue in actual typescript code as opposed to either the typings or the JS and cross referencing.
Improved source maps - For users doing their own compilation they'll be able to integrate the redux source into their build process as opposed to using our precompiled dist source. This has the end result of producing better source maps in complex build environments.
That's what redux-starter-kit is to me. We've already got it, just with a different name.
I'm not sure about rewriting Redux in TS. The typings we have already are pretty sufficient and very well-tested. Just because we don't "use" them doesn't mean they're not trustworthy. Also, this puts users of other type systems out in the cold. I'd rather maintain a "least common denominator" of plain JS. It's more readable for the vast majority of our users.
I'm not following the argument on source map support. That's already built-in, depending on what package file you pull from. If that's a module (most common for bundler setups nowadays), the source of that file is pretty readable. Just some minor changes by Babel with intact import/exports and comments.
What if we were to rewrite the core lib in TypeScript?
I think this would be a very good idea, and I would be willing to help do it. It would go a long way to improving the types, IMO. However, you might find you end up changing the API somewhat when doing this, as designing with types in mind from the start would probably lead you towards making some different design decisions.
Anecdotally, I've seen a lot of people hand edit the Redux typings (and not PR back to source) because something doesn't line up with what they expect.
Agree! Personally, I think there is too much any
usage and too much defaulting of generics. It sort of seems like the ordering of generics & giving them defaults almost everywhere as a convenience were given priority over realistic usage, type safety, etc.... I think this is why so many people write their own types instead.
Would also be against immer; the docs could talk about it in greater details, in section that would also talk about performance improvements toolset, like reselect.
I agree with this. Not everyone wants to use Immer... I understand that it's convenient for people who prefer to work with an imperative/mutative API, but not everyone wants that.
I'm of the opinion that the core should be as minimal as possible. Move out things like combineReducers, bindActionCreators, and compose into their own libraries (or pull from something like lodash in compose's case).
Also, agree with this. Especially about compose
. It always seemed sort of silly that Redux ships with its own compose
function when so many utility libraries (lodash, ramda, fp-ts, etc.) already implement it.
FWIW, just be aware that compose
has problems with TypeScript (and Flow)... TS is not able to properly infer from right-to-left with curried arguments, or from right-to-left with non-curried arguments if a right-ward function is not annotated. So, it requires more diligence... you pretty much want to annotate all functions if you're going to use it.
Rewriting Redux is in a typed language could prove valuable in influencing a different design or API.
My experience with Redux and its typings aren't necessarily a bad one--I've found Redux's third-party typings to be sufficient enough in expressing what it does. The main issue I've encountered is the type boundary across dispatch
and the reducer. Since middleware may transform both the return result of dispatch
and transform the action into different actions, I've done certain tricks to eliminate the type boundary:
dispatch
which combines our AppState and middleware actions with their correct return types:interface AppDispatch {
<R>(action: AppThunk<R>): R
<T, U>(action: ApiClientAction<T, U>): Promise<
{ payload: T } | PackError<ApiFetchError>
>
<T extends Action>(action: T): T
}
const someCreator = makeActionCreator('MY_ACTION')((arg: string) => ({
type: 'MY_ACTION',
payload: arg,
})
// .. in the reducer ..
if (isSomeAction(action, someCreator)) {
// .. here typescript can infer the correct type for `action`
}
I wrote about this kind of approach here: https://medium.com/@danschuman/redux-guards-for-typescript-1b2dc2ed4790
If Redux were to be written with types as first-class, we could possibly avoid some of these hoops?
One more thing... While we're on the subject of more radical changes, rewriting in TypeScript, etc., I would love to see this API made available in vanilla Redux:
ReasonReact.NoUpdate: don't do a state update.
ReasonReact.Update(state): update the state.
ReasonReact.SideEffects(self => unit): no state update, but trigger a side-effect, e.g. ReasonReact.SideEffects(_self => Js.log("hello!")).
ReasonReact.UpdateWithSideEffects(state, self => unit): update the state, then trigger a side-effect.
Also, found in react-recomponent
:
And in purescript-basic-react
:
https://github.com/lumihq/purescript-react-basic/blob/master/src/React/Basic.purs#L165-L169
As @quicksnap mentions above, although the overwritable/overloadable dispatch
design in Redux opens up a lot of power/extensibility via middleware, it also introduces a good bit of complexity & difficulty in a statically typed environment.
However, at this point, obviously there are way too many really popular middleware libraries out there to just abandon that design, but maybe a new set of dispatch
functions resembling the ReasonReact action update API could be made available alongside the one intended for middleware in order to make the lives of TypeScript, Flow, etc. users easier?
It could be as simple as exporting separate "dispatch"/"update" variations which have their own type signatures & can't be modified by middleware, but the SideEffects
and UpdateWithSideEffects
variations would require a new post-reducer-update hook to allow running the passed callbacks as effects. This would be a somewhat bold change to the API, BUT it would really just be an addition and no one would be forced to use it (though I think it would become popular). The fact that it's been implemented in a number of languages and libraries already suggests that it's a useful design, IMO.
See my comment here in an open issue for the react
hooks API where I explain how this could be useful and could help obviate the need for something like redux-saga
or redux-observable
a little bit: https://github.com/facebook/react/issues/14174#issuecomment-447762929
It, of course, isn't as powerful of an API as what those middleware offer, but it does give you the capability to fire off (potentially async) effects through Redux while keeping your reducers totally pure. This would add a lot of flexibility to Redux.
I've implemented this before on my own using the class API + the newer Context API + setState (wrapping setState
to create dispatch
& tapping into setState
's callback param as a place to run these effects), and it worked out great. However, the implementation in the above linked react-recomponent
is very similar to what I did and could serve as somewhat of a guide for how to approach recreating something like this in Redux.
I think that including redux-thunk
(even just moving it into the package, not automatically adding it to middleware) would be reasonable. The docs refer to it as "the standard way to do [async]."
I know there are a lot of options with async, and I think part of the reason that there are so many is that, for most cases, the choice of pattern doesn't matter a great deal. In any case, it's clear to me that we should let problems drive our solutions, and asynchronous effects are always part of the web dev "problem."
(While I'm on the topic: nothing is free, and extensibility is a trade-off. e.g. React doesn't provide redux-style state management. Pro: you can use whatever you want. Con: for something like redux to work, it needs something like react-redux
. The result is that asking pieces of your application to read the application state is heavyweight ¯\_(ツ)_/¯ )
Anyway, micropackages aren't, in my mind, a goal in their own right. Along those lines, I have no problem with the current inclusion of compose
, or combineReducers
. But I can't speak to the ease of using either in TS.
...since you asked ;)
Wow. How is this thread even still open? Clearly I haven't been paying any attention to issues in the Redux core repo :)
At this point we're obviously not going to add anything else to the core, but I'm open to adding some more pieces to Redux Starter Kit.
Note: This idea is almost entirely hypothetical, but it's been suggested a few times and is worth discussing.
The Redux core has been tiny since the beginning. The original intent was always to keep the core small, make the API extensible, grow an ecosystem, and maybe "bless" some plugins along the way. (References: The Tao of Redux, Part 1: Implementation and Intent, You Might Need Redux (And Its Ecosystem))
This has worked great, in the sense that there is indeed a huge ecosystem of addons for just about any use case you can think of. As part of that, the core library package has stayed basically unchanged for the last three years, and the docs have been very unopinionated (usually just listing some available options, like folder structure approaches).
We've recently introduced our new
redux-starter-kit
package. It adds some deliberately opinionated utilities around the Redux core, like simplified store setup with the most commonly used options and some sanity checks, simplified reducers with lookup-table definitions and Immer-powered "mutative" immutable updates, and even creating entire "slices" of state at once. While it hasn't gotten widespread usage yet, the reactions I've seen have been almost universally positive.As part of that, some people have suggested that we might want to consider actually renaming the current
redux
package to something like@reduxjs/core
, and renameredux-starter-kit
to be the newredux
package. I recently ran a Twitter poll asking about this, and the results were a bit surprising. 54% of actual respondents were in favor of this idea.I'll be honest and say that this is not very likely to happen. But, it's worth opening up a discussion about what actual additional functionality is worth adding to the core, if any.
So... thoughts? Ideas? Suggestions?
As a reference, I'll link issue #2295: Request for Discussion: Redux "boilerplate", learning curve, abstraction, and opinionatedness. That overly-long thread does give some indications as to the kinds of things people might find useful.
Tagging @modernserf and @matthew-gerstman , since I know they have opinions on this, and of course @timdorr , @gaearon , and @acdlite .