How to handle a global state a la Redux in Reason?

reasonreact

#1

I like reason but I don’t know how should I handle the app global state like I do with redux


#2

I’m using the Reason React Context library the the same way that you would in React JS v16. Once reason-react gets support for context, this should be easy to migrate.

I’ve used a globalState record that is passed down into context when the app is initialized. I pass the global state record and also a setGlobalState function in the context that can be used to update the state.

One note, this library will re-render all the children so if performance is a factor you may need to pass props instead.

This is not a great example because there’s a lot more going on but I setup context here:

and also send it down to the children like this:

and the children can consume it like this:


#3

FWIW, here’s an older discussion (I haven’t parsed it yet) on global state: What's the best way to handle global state in ReasonReact?.


#4

By reading that I resume that there is no a clear and standard way of doing this at the time


#5

@mrkaspa Until we have more information on what is to happen with context (it’s future seems unclear), there is no clear way, that’s correct! Just choose what works best for you at this point.


#6

I wonder if context is even all that crucial. The way I see it, the main problem it solves is prop drilling, and prop punning should alleviate that pain to some extent.

Also, I wonder if The Elm Architecture that inspired Redux in the first place should work even better in ReasonReact. As in, unlike Elm, you don’t have to map actions/messages all the way up to the root variant and then go all the way back down from the root reducer to the local one.

Sure, prop drilling is still an overhead, but so is working with context.


#7

Yeah I also think that typing will help with this (and shoudUpdate undefined data errors) because a pain i’ve came across in react was making sure all the components were updated after refactoring or adding a new feature.

Context seems to work best for things that are truly global like modals, viewer profile data, etc… I think using it on forms is a stretch unless you have a library or have a lot of form inputs to handle. I’m sure the community will have to over-use/abuse it to figure out where that line is :smile:

IMHO Redux is a drastic reaction to setState because a lot of people think you have to have your entire app state in a Redux state tree. Having state per feature (or section of an app) and prop drilling has worked out pretty well for a lot of people (including me).


#8

Hey, that reminds me: one of our projects decided it’s much less hassle to maintain a separate Redux store per page, even if it means duplicating data between pages :grin:

And, yeah, personally I do not think you have to put everything in Redux. Local state is fine, even with, say, TypeScript.


#9

I’ve used a globalState record that is passed down into context when the app is initialized. I pass the global state record and also a setGlobalState function in the context that can be used to update the state.

One note, this library will re-render all the children so if performance is a factor you may need to pass props instead.

So basically if I save user data globally (like in the example from author of Reason React Context) every time that data changes, everything re-renders?

Even author says:

This is a todo app using the ReasonReactContext to pipe global state

I would not recommend doing this but you can :stuck_out_tongue:

If I understood everything correctly using this library would not be preferred way to handle Global State - maybe for some things that really rarely change and actually have benefit from re-rendering like author’s theming example. Please correct me if I am wrong :slight_smile:


#10

With the Reason React Context library it will, so if performance is critical on day 1 I wouldn’t do this. However, once we get the real React context from v16, you can remove the library and from what I understand, then only the connected components would re-render when the data changes.

Also if the user data frequently updated and you’re using the context library in the mean time it may kill perf.

I think this has to be ironed out in JS land first to see what the major pitfalls are. However, for a lot of apps you can replace redux with context and local state. Just my opinion though… there’s no standard way :smile:


#11

Thanks for clarifying :slight_smile:


#12

Also after re-reading my posts, I also failed to mention i’m using Apollo GraphQL for all of my server state.
This also ties into the OP’s @mrkaspa question about global state like Redux.

The Apollo cache (which uses Redux) stores a lot of data that I used to store globally in Redux when I used REST. Only now Apollo normalizes it for better performance, caches it for instant re-use, optimistic updates to the UI, pagination, and it’s API makes it more clear what data a component is using.

For the remaining state (for me, loading indicators, widgets like modals, confirm boxes), having setState works for great for that and Redux isn’t something you’ll really have to have on most apps.

The killer feature for me is Reason can now create compile time errors if you mis-use the Apollo GraphQL data… a huge maintenance win.


#13

Correction: Apollo 2.x doesn’t use Redux anymore. It uses apollo-cache-inmemory by default and you can use other caching packages like apollo-cache-hermes (experimental).

Also, as of v2.1, Apollo switched from HOCs to render prop components, which made it way more convenient IMO. I haven’t looked at Reason bindings for Apollo, maybe 2.1 is not there yet, but if some of your projects use Flow or TS, have a look :wink:


#14

I’ve been very happy passing down props from the root component to wherever it needed to go. This is made especially easy because of JSX punning so that I can simply write <LayerPicker uid onHoverLayer onSelectLayer /> .

We could also just pass self.send down instead of creating explicit callback handlers for each function. This will eliminate a large amount of boilerplate. So the code will look <LayerPicker uid dispatch=self.send>.

Another optimization is to wrap related things together in a record type, and make them opaque by putting them in a module with a tiny interface. This works very well if there are actual domain objects and functions that operate on them.

Having written a fair amount of Reason in the past one year it seems that “prop drilling” is simply functional programming doing what it needs to do - make dependencies explicit. Implicit context like Redux’s connect and the Context API goes against that principle. While implicit context might be easy to reason about in the early days of a codebase, it might grow to be unwieldy as the system grows.


Poll: State management: Redux, MobX, ReasonML
#15

Is self.send bound to self? Or does BS use closures instead of this?

And anyway, doesn’t it make your components coupled?


#16

This is a really great idea. When you say make them opaque what do you mean here? I’ve seen opaque types in Flow and generally understand how they work but how does that work in Reason? Also do you have to create a new module or could you just use a type postsProps = {post: Post, foo: string, updatePost: ...};?


#17

I don’t have a lot of clarity there, but I think self.send carries a closure with it. But so should any handler function that is passed down.

You also have to keep the state and actions in a separate file so that multiple components can all refer to it without introducing cyclic dependencies.

It does make the components coupled, and I’m reluctant to use it. I’ve ended up switching back to passing plain handlers. It is just an option to consider before going for a Redux like approach.


#18

Here’s an example:

module DownloadSchedule = {
  type downloadUrl = string;
  type status =
    | Idle
    | Scheduled(downloadUrl)
    | Downloading(downloadUrl)
    | Cancelled;
  type t = {
    id: string,
    status,
  };
  let create = (id) => {
    id,
    status: Idle
  };
  let initiate = (id, onSuccess) => {/* code */};
  let poll = (t, onDownloadAvailable) => {/* code */};
};

Now if you create an interface for this module and write:

type t;
let create: string => t;
let initiate: (t, t => unit) => t;
let poll: (t, t => unit) => t;

Note the type t here. It is a phantom type (/opaque) because we’re not specifying what the type actually is. This means no other module has any visibility into its innards, and so can’t pluck anything from inside it. This is true encapsulation as enforced by the compiler. Other modules can call create which will give it a t, and then pass that t around. But when you need something from inside it, you need to ask one of the exposed functions to get that for you (there are no such functions in this example).

More about this technique here: https://blog.janestreet.com/howto-static-access-control-using-phantom-types/

Once you have defined a type like this, you can put it in a ReasonReact component state, and pass it around as a single well-encapsulated value. You can write all your domain logic in this module and provide them lambdas that can call into any UI related code, keeping the business logic cleanly separated.


A simpler cleaner way to update a nested state object in a reducer?
#19

Oh wow this is great! I was just thinking last night about how I would go about separating out the business logic so that in theory… I could build all the main core of an app without a UI and then somehow pass that into the view layer (reason-react) to add an interface for that business logic. Great stuff and many thanks! :+1:


#20

Quick correction: t in the above example is just an opaque type, not phantom.

Another way to avoid passing down parameters through a deep hierarchy is to use children as explained nicely here: https://daveceddia.com/context-api-vs-redux/