rickyvetter / reductive

Redux in Reason
MIT License
409 stars 40 forks source link

Usage discouragement in readme #26

Open rusty-key opened 6 years ago

rusty-key commented 6 years ago

Hi!

Sorry if what I am about to say is complete rubbish. I am new to reason and reason-react and maybe I just didn't grasp some basic concepts yet.

In the intro you are saying:

You might not need this library, especially so in a language which provides good enough construction blocks out of the box. ReasonReact already comes with reducers!

I don't understand this point. You are talking about built-in reducers like it is an alternative to a global state. But we are mainly using redux for managing app state, not a component state. In what way built-in reducers are the replacement for redux?

If I understand Dan's point that you referenced, he is not saying that you don't need redux. He is saying that you maybe don't need redux. But there are a lot of cases when you can benefit from using it.

And then later you are saying:

Hopefully, the above comparisons demonstrated that you might not need an extra-react state management technique at all; passing props down works well in Reason-React, and since props are well-typed, adding/remove them takes seconds. Keep state/props management simple so that you can spend the learning budget elsewhere!

We are using global state when an application starts to grow past the point when passing props down starts to be unhealthy, when you want your components to be agnostic to place and time. Imagine moving component from one part of your app to other. In redux-app you just moving it, that's it. In "classic-react-app" if you don't have single-global-compositor-component which basically have your global state, you have to tune something. Your middleman components have to know something that they don't need to know, just to pass some props down.

Am I missing something? Maybe you can elaborate a little bit more in your docs as to what real alternatives we have to reductive and why using reductive is counter-productive in reason-world?

It is kind of depressing to use a library that asks me not to use it and with bold font :)

conartist6 commented 6 years ago

Not complete rubbish! Actually your bug report here is more informative than either the Reason or Reductive documentation as to what the limitations of Reason's reducerComponent are, what an application which opted not to use Reductive would look like, and what some indications for the use of Reductive might be.

conartist6 commented 6 years ago

Also the more I think about this the more I disagree with the conclusion that Reductive probably isn't necessary.

If you just create a global compositor/store component, you are indeed stuck passing around global state as props through a chain of components that may not actually depend on them.

One of two things will happen:

This wouldn't be particularly important for regular React, since most regular React components (React.Component and stateless functional components) rerender regardless of prop equality, but it is important to ReasonReact, which by design is meant not to have the fundamental problems that have forced regular React to be unable to make those optimizations.

quicksnap commented 6 years ago

This is a great discussion to be had!

I'm a huge fan of Redux. In my work, it has offered me these advantages:

Preventing prop-passing and unnecessary coupling via props

As mentioned, passing a bag of the entire state into a component which needs global state feels unwieldy. It tends to yield a pattern of every component in the app needing access to the global state object, which is unnecessary when we have React context.

Common idioms + Middleware

The redux community sharing ideas, patterns and libraries involving middleware have been a huge productivity boon for me. Examples have been things like redux-pack and homegrown API middlewares that originated from an example project. A new tool I've used recently was redux-first-router, which exposes a reducer/middleware/action-set that you can use in your app. Allowing libraries to take use a slice of your state/reducers has some real benefits.

Reductive could actually enable the same type of patterns. Leveraging Reason's extensible variants and possibly GADTs, there's nothing preventing Reductive from allowing third-party actions into the system while maintaining type safety.

Redux Devtools

This is simply something I cannot imagine living without on the frontend. It has saved me so much time, and made debugging my Redux application so much easier. Unfortunately, Reason+Bucklescript does not have the means for this yet. I've opened https://github.com/BuckleScript/bucklescript/issues/2556, but there's so much work on Reason/Bucklescript core teams' plate as it is.


Redux enables the above patterns successfully. While ReasonReact may be able to prevent unnecessary re-renders while passing down the entire state object as a prop (or even as context), the common idioms and dev-tooling built around those idioms in Redux offer significant value to me.

I'm eager to improve this library, but I view it futile until there are dev-time printing mechanisms available in Bucklescript.

To the point of the bold warning that "you may not need this library", I feel it's meant to bring about critical thought like this as to what is state in this language and ecosystem, and how can we leverage these tools to come up with something better. I feel Reductive could evolve to take the best of both worlds (Redux and ReasonML), but there could be something even simpler and better that we think up.

jeremyjh commented 6 years ago

To the point of the bold warning that "you may not need this library", I feel it's meant to bring about critical thought like this as to what is state in this language and ecosystem

Well, this is a good warning that should be at the top of the Redux repository as well frankly. Small apps are better off with component state and newcomers to React should definitely start there.

What I get from the README, on the other hand, is "you don't need a state manager because Reason". This is confusing and frankly, just wrong. Nothing about Reason/OCaml changes any of the very excellent reasons articulated so well in this issue.

In fact, it makes the project look a bit out-of-touch with the purpose of a state management framework and makes me wonder if it is implemented correctly. For example are components only re-rendered when the part of the state graph they care about is changed? Since there is mention of no higher-order connect, and no acknowledgement that such a feature is desirable, I have to assume that no, this project doesn't understand why that feature would even exist, and I move on to the next language/framework.

conartist6 commented 6 years ago

Oh jeez, I made a mistake, mostly because I'm looking at Reason from the perspective of thinking about using it, rather than as an experienced user already.

I thought Reason components would not adopt the React pattern of always rerendering no matter what. Unfortunately, they do. See https://github.com/reasonml/reason-react/blob/9e344b8a059ca82e88ca575bebc60abb9deac0d8/ReactMini/src/React.re#L134 and its usage just below.

So not using Redux/Reductive is just a performance bomb waiting to go off, for both React and ReasonReact. I'd put the warning on React instead (this library is just a child's toy without a state management utility!). A solid piece of JS infrastructure should be simple when used the recommended way: i.e. when you follow best practices it should shield you from the nastiest complexities of lower layers and you should get a performant app which you won't need to rewrite (as in the transition from setState to Redux) when your needs become more complex.

jeremyjh commented 6 years ago

@conartist6 From a quick read of reductive source I do not think it is going to do that for us either. It renders as long as the State is populated. react-redux accomplishes this render on changed state behavior through the higher-order connect component - this is why we have the mapStateToProps ceremony. Without that or something like it I don't see how the framework can know what data is actually being used in the component.

conartist6 commented 6 years ago

I now agree @jeremyjh.

So the usage discouragement is actually a great point. There isn't any purpose or intent to what this code does.

Edit: aside from allowing Reason code access to state already stored in Redux, presumably by code not written in Reason, and to enable dev tools.

JasoonS commented 6 years ago

I haven't started using ReasonReact or Reductive (disclaimer), but I'm super glad I read this.

But it is true, writing mapStateToProps is a 'ceremony' that makes us conscious of what state we want in our components. Similarly working with this Reductive and making the app run efficiently would require people making a 'ceremony' of shouldUpdate method. Although it must me said that the ceremony for react-redux is to be favoured since it is explicit, whereas currently in Reductive such an optimisation is hidden and non-obvious.

So that seems it a bit of an extreme conclusion there, @conartist6 ?

There isn't any purpose or intent to what this code does.

conartist6 commented 6 years ago

Well, you create the rather unpleasant issue that if you forget to adjust shouldComponentUpdate when you use a new piece of state data you'll get stale data.

However this does make clear to me one thing that Reductive does in this situation, which is to allow a child component to receive updates on a piece of state that their parent elected not to update on. If you were just passing your global state object around through props, that would not be the case.

jeremyjh commented 6 years ago

That's right. mapStateToProps doesn't ONLY let react-redux determine what has changed, it also enables your component to access the data it needs. While it could contain data you don't use, it is not possible for you to use data that react-redux doesn't know about. This isn't true for reductive.

bonham000 commented 6 years ago

I agree with a lot of ideas presented here. ReasonReact looks very promising to me but the main limitation that I don't see clear answers for is how to manage large apps with complex state trees. Redux provides a strong answer for this in the traditional React world, which to me is not matched by managing app state in reducer components in ReasonReact.

The following are some of the main considerations I have which currently prevent me from considering ReasonReact for a complex app:

I think in the React ecosystem there was a timeline where initially React did not endorse a clear implementation for managing global state, instead suggesting a pattern (Flux). As developers saw the limitations in managing application state in React components, they innovated to create solutions and this has produced everything we have now: Redux, React-Redux, various middlewares, devtooling, and so on, which provide great choices for building a complex JavaScript application.

I think ReasonReact will need a similar progression for the community to begin adopting it as the foundation for their applications. Maybe the solutions will look different, but I think the main principles of decoupling state and side effect management from the component/view layer and providing composition so the these layers are scalable, are very important.

An example application where I think these considerations start to make sense are say an app with several hundred components, 50-100 Redux stores, and maybe 50-100 different HTTP methods that send and receive data from various APIs.

ariagaming commented 6 years ago

This is a great discussion. I've been using React and Redux for years now, both in production and for fun. I understand why we use the flux pattern in React through Redux, but what I can't understand is why, with Reason, we don't use proven FP patterns instead of leaning on the old way of doing things...

We should be using: 1) State Monad, lift your render functions into a stateful realm. Instead of connect or reducerComponent use boundComponent which just fmaps your render function with the state monad... 2) Lenses, lenses are a very nice way to inform the code which part of the state you are using, giving you a solid base for choosing to rerender your component. 3) Put 1 more layer between your component and your reducer; I call it the "Action Monad", here you compose actions which you can call from your views which can "prepare your models for update" (read: lot's the .then's on a Promise) and after the Action completes; send the result to reducer, reduce it, rerender when needed.

With the extra layer for manipulation (point 3), data fetching, randomness, ffi and the Blizz Gods only know what else; combined with real world FP ideas you can have an amazing experience and very powerful type safe framework. While still being very easy to learn. Easier than Elm, much easier than PureScript, might even be easier than normal React and Redux 'cause of that whole "don't pass lambdas as event handlers" business. That stuff is silly hard!

Conclusion: I really think the community is searching for an alternative to JavaScript. I totally agree that OCaml is way better than Haskell inspired languages for beginner, intermediates and maybe even experienced programmers. Elm takes another route and just doesn't give you the power you really need to express yourself in your applications. ReasonReact should be a home run, the language is great, react is great, FP is awesome...I smell a winner!

I apologise in advance for intruding on this thread. I hope I haven't offended anyone. In any case, just my two cents.

(Extra cents: 4) Get rid of the Component syntax. Just lift your functions and de-sugar at compile time. No reason for that boiler plate it is distracting and not effective. 5) Remove the lifecycle methods, I mean all of them. We can lift and shove and bind and fmap if we need to, but 99% of our code should be covered with the state monad and lenses. )

nlfiedler commented 6 years ago

@ariagaming

  1. Is this boundComponent/fmap idea similar to render props?
  2. Yes, agree that lenses are a splendid approach. The Rationale Lens.re looks promising.
  3. The additional layer sounds like redux-saga/redux-observable, am I right? Absolutely agree that something of this sort will be needed.
nlfiedler commented 5 years ago

In case anyone is interested, I attempted to address the render issue in pull request #35. Please let me know your thoughts.

nlfiedler commented 5 years ago

Tried to remove the warning, but it seems it's been discussed and honed to its current state. For now, we can describe why reductive is useful, and let people decide for themselves.

rickyvetter commented 5 years ago

Hey folks - sorry not for weighing in on this sooner. The message is indeed put in place so that people actively think about their use case before reaching for this (or any other) state management library.

We are using global state when an application starts to grow past the point when passing props down starts to be unhealthy

Who makes the decision about when/if this is unhealthy? I personally think that no amount of props passing is unhealthy, but you might feel differently. I do think that implying that it is unhealthy is biased toward libraries like redux unnecessarily. Someone with more context on what they are building should make those decisions.

when you want your components to be agnostic to place and time. Imagine moving component from one part of your app to other ... Your middleman components have to know something that they don't need to know, just to pass some props down.

Again I feel like this is pretty subjective. I like my components to loudly express their data dependencies and be as pure as possible. If a component is using some data (even just to pass) I want that to be visible to the people refactoring.

I think the arguments for Redux that Dan makes in his blog post are pretty strong - but they are all heavily dependent on the context of the app you're building. Does your app need to persist entire state to local storage? Do you need time travel debugging? etc. We just want to make sure that this project doesn't get installed next to ReasonReact as a default without thought. I'm open to other strategies to get there, but I don't think removing this caution without doing something else is going to achieve that careful consideration we are hoping for.

jeremyjh commented 5 years ago

@rickyvetter These are all great considerations but none of them are unique to Reason/OCaml. The verbiage in the README is specifically telling us: You may not need global state management, because Reason. That statement I think, is incorrect. As I mentioned in my first post in this issue, I think these (and other things mentioned) in this issue are all great considerations and it would be nice if Redux articulated them as well. I would recommend just changing that verbiage a little bit so you aren't trying to defend the notion that Reason is a special case when it comes to global state management.

rickyvetter commented 5 years ago

You may not need global state management, because Reason.

I don't think that is ever specifically said or implied. The caution says nothing about global state at all. The part about Reason building blocks is referring to first class algebraic data types. A lot of people are interested in Redux for the reproducible/standardized pattern of actions. This isn't necessary in JS either, but I think it's worth calling out that Reason/OCaml have this enum w/ payload first-classed into the ecosystem.

I can't think of great ways to modify this, but definitely open to reviewing changes.

jeremyjh commented 5 years ago

A lot of people are interested in Redux for the reproducible/standardized pattern of actions.

Good point. I guess I never thought of using a library like reductive for anything other than global state, so probably I was reading more into it than it really says.

mnieber commented 5 years ago

@rickyvetter I'm confused now. If reductive is intended as a "authoritative Redux implementation", then it's (not deliberately, but in a practical sense) misleading when it doesn't support the main use-cases that Redux supports.

rickyvetter commented 5 years ago

@mnieber which use case(s) do you feel Reductive isn't supporting? I'm not sure I understand which cases you're thinking of. Is this in relation to a different issue? This one is specifically about the question of documentation.

mnieber commented 5 years ago

I meant the use-case where components (aka container components) connect directly to the global application state, instead of receiving this state as props from parent components.

EDIT: it's a bit strange to call this a use-case, maybe "typical usage pattern" would have been a better term.

By the way, there is another characteristic property of Redux which I feel is also not supported out of the box: the ability to combine reducers and still let each reducer process all actions. In Redux, this is the default behaviour. In the reductive examples, there is a dispatch mechanism that lets specific reducers respond to specific actions.

rickyvetter commented 5 years ago

Agreed that usage pattern is a better term for the things we are talking about. I don't think reductive stops you from connecting each of your components directly to global state without passing props or from having all of your reducers act on the same top level actions. It just doesn't show any examples like that.

I don't think this is super relevant to the discouragement in readme conversation though. I think if you want to continue discussing it'd be best to open a new issue.

rusty-key commented 5 years ago

@rickyvetter, I think this discussion is in the spirit of my original message and doesn't need to be moved to a new issue.

The problem is not in discouragement itself. The problem is what it is implying. I read it like "You might not need this library because ReasonReact comes with reducers". But these reducers are component-level, not global, so they are not covering the main use-case as @mnieber pointed out. And the existence of these reducers is not the reason to not use the library. The reasons are different and described in the article linked in the discouragement!

rickyvetter commented 5 years ago

While I agree that it shouldn’t, I think the existence of reducers does cut prevent a lot of people from turning to a Redux-like library. The idea is to turn people away who are chasing fads or exciting libraries - or at least make them think! If you’ve decided that a Redux-like library is right for your app and that reducers don’t change that, then great!

Also, to be honest I don’t see any of the reasons described in that article as difficult to do with hooks and some appropriate introspection into React internals. useReducer and useState allow you to do all of that cool stuff without Redux.

rusty-key commented 5 years ago

The idea is to turn people away who are chasing fads or exciting libraries - or at least make them think!

The precaution is misleading. It implies that you can replace this library with ReasonReact reducers, but you can't except for some specific case. The main case because of which people turn to Redux is not supported by ReasonReact reducers.

At least consider adding a bit of clarification:

You might not need this library at all. Consider using local state, especially so in a language which provides good enough construction blocks out of the box. ReasonReact comes with reducers!

I would even add a sentence or two about ease of passing props in ReasonReact as an alternative to global state. What do you think?

mnieber commented 5 years ago

I must say I'm still confused :-). It's not clear to me a) how different Reductive really is from Redux, b) how much effort is required from the user to implement typical Redux patterns with Reductive, and c) how well this would perform.

However, I'm not advocating for any deep analysis of the differences (such an analysis would be nice of course but not essential). Instead, it would be great if the README could make some prediction of how much effort a typical Redux user (who is using Redux in the way that Dan Abramov teaches it to other people) would require to re-implement their (global) data store with Reductive. This would allow people who are "just" looking for the best tool to make a practical decision.

rickyvetter commented 5 years ago

At least consider adding a bit of clarification ... I would even add a sentence or two about ease of passing props in ReasonReact as an alternative to global state. What do you think?

Yeah sounds great!

it would be great if the README could make some prediction of how much effort

I don't know if I'm capable of making such a prediction, as I am not a typical Redux user. Happy to accept PRs if someone does have/can make this estimation.

a) Reductive at its core is almost identical to Redux (how different can you be in ~50 lines?) - but differs in that it doesn't encourage a lot of the same patterns. b) again - I'm not the right person to ask here c) Reductive the library will always perform about as well as Redux. The high level patterns will always be implemented the same way.

There are a couple reasons Redux might perform better:

  1. more eyes on it so any perf optimization things have probably already been fixed
  2. if you (incorrectly) mutate state the updates will be much quicker - it's easier to mutate in JS than it is in Reason.

There is one place Reductive will be faster:

  1. Reason immutable updates are faster than JS obj/array spread unless you are compiling with Babel loose mode or other optimizations. The JS versions have to do a lot of extra work to deal with the dynamism of JS.
TomiS commented 4 years ago

So, it's almost 2020 and everyone's using context now. Correct me if I'm wrong but isn't the way the context behaves so that whenever anything in context changes, it will trigger a re-render in all components using the context. It feels to me, that for larger apps, it makes it pretty much unusable at least as a trivial solution. Maybe if one splits the context to multiple nested providers touching a different area/module of the app, then it might work, but if one just uses single context for the whole app state, then all changes to the global state will practically re-render the whole application every time. (Am I missing something here, honestly?). If this is the case, using reductive starts to feel pretty tempting assuming it actually solves the problem.

Another thing that would be relevant to document and clarify... or at least I would find it tremendously useful... would be how to trigger side effects that are not bound to component lifecycle (i.e. useEffect). It seems to be very hard to find examples or guidelines. I see there are some ways to use thunks with reductive, but what I'm more interested in is something like redux-sagas or anything that solves the same problem. It is true that using something like these makes the code harder to follow, but it also allows ways to e.g. prevent unnecessary reloads of data independent of how components are mounted/unmounted.

I guess I'm asking whether there are now any established patterns/solutions now that one year has passed since the last message in this thread. Oh... and I know there is GraphQL... but let's assume not everyone can use GraphQL :)

mnieber commented 4 years ago

@TomiS I don't have any data on this, but I expect that using context (which was always there, by the way) hasn't replaced the default patterns for connecting to the data store. Also, I think that using context should be the exception.

MargaretKrutikova commented 4 years ago

@TomiS You are not missing anything, using context and useContext doesn't allow to bail out of re-rendering the same way useState does, so if you have components that are only interested in subscribing to a part of the state in your context, they will still re-render if anything on the state changes. Moreover, react-redux tried to use context to propagate state updates in v6 and eventually switched back to custom subscriptions in v7 due to performance overhead introduced with context.

I agree that neither redux (or react-redux) nor reductive have a clear recipe for dealing with side effects. I am not aware of any redux-saga-like solution in reason, or any other established solution.

I was experimenting a bit and implemented the elm architecture with reductive in a little library reason-react-elmish, that allows your reducer to return both the new state and an effect, that will be run by the library. Similar to reductive, there you define model, update and message (if you are familiar with elm) which correspond to state, reducer and action .

Maybe it is something that might be helpful in your case?

TomiS commented 4 years ago

@MargaretKrutikova Thanks for confirming my thoughts. Also, reason-react-elmish looks very interesting. I'm absolutely going to try to fit it into the app I'm working on atm.

I'm looking at the example in the repo. I'm assuming that message == action, model == state and update == reducer? Truthfully, I'm not quite understanding how the success action actually changes the data in the store though. But nice work anyway. Going to look into it in more detail later.

MargaretKrutikova commented 4 years ago

Yes, those are the concepts coming from elm, where your reducer (update function) will return a tuple with a new model and a command, which translates into state and a side effect.

When you get back the data from the api, FetchUsersSuccess gets dispatched and processed by the reducer here:

let update = (_, message) => {
  switch (message) {
  | FetchUsersSuccess(data) => ({data: Success(data)}, None)
   ...
  };
};
TomiS commented 4 years ago

@MargaretKrutikova Ah, you don't need the previous state at all because in this quite simple example you're replacing the whole state on each reducer call. The lack of previous state confused me. Slow brain on Sunday, it seems. :) But thanks again.

ambientlight commented 4 years ago

I am not aware of any redux-saga-like solution in reason, or any other established solution.

@MargaretKrutikova Just saw this one and decided to publish redux-observable port to reason I've been using internally. Take a look: https://github.com/ambientlight/reductive-observable

spirobel commented 2 years ago

I dont know if you follow redux toolkit maintainer Mark Erikson and his work? He recently did some work to accommodate more usecases that are currently served well by redux-saga. Please take a look at this for further reference: https://github.com/reduxjs/redux-toolkit/discussions/1648 An objection against redux saga is that the generator yield syntax is too much to learn for people. Currently redux saga does not work well with typescript. So it would be great if rescript would fill this void. It could also address the syntax criticism somewhat and replace it with the criticism that you have to learn the reasonml syntax lol. But in this case there would be the clear tangible benefits that come with it like type inference and all this stuff. While at the same time you would get all the tooling and easy testability that come with redux and redux-saga. I also posted about this in the rescript forum: https://forum.rescript-lang.org/t/rescript-syntax-for-generative-functors/644/10?u=spirobel

Thanks, Spirobel