State, Actions & Reducer
Finally, we're getting onto stateful components!
ReasonReact stateful components are like ReactJS stateful components, except with the concept of "reducer" (like Redux) built in. If that word doesn't mean anything to you, just think of it as a state machine. If that word does mean something to you, just think: "Woah this is great".
To declare a stateful ReasonReact component, instead of ReasonReact.statelessComponent("MyComponentName")
, use ReasonReact.reducerComponent("MyComponentName")
.
Here's a complete, working, stateful ReasonReact component. We'll refer to it later on.
/* State declaration */
type state = {
count: int,
show: bool,
};
/* Action declaration */
type action =
| Click
| Toggle;
/* Component template declaration.
Needs to be **after** state and action declarations! */
let component = ReasonReact.reducerComponent("Example");
/* greeting and children are props. `children` isn't used, therefore ignored.
We ignore it by prepending it with an underscore */
let make = (~greeting, _children) => {
/* spread the other default fields of component here and override a few */
...component,
initialState: () => {count: 0, show: true},
/* State transitions */
reducer: (action, state) =>
switch (action) {
| Click => ReasonReact.Update({...state, count: state.count + 1})
| Toggle => ReasonReact.Update({...state, show: !state.show})
},
render: self => {
let message =
"You've clicked this " ++ string_of_int(self.state.count) ++ " times(s)";
<div>
<button onClick=(_event => self.send(Click))>
(ReasonReact.string(message))
</button>
<button onClick=(_event => self.send(Toggle))>
(ReasonReact.string("Toggle greeting"))
</button>
(
self.state.show
? ReasonReact.string(greeting)
: ReasonReact.null
)
</div>;
},
};
initialState
ReactJS' getInitialState
is called initialState
in ReasonReact. It takes unit
and returns the state type. The state type could be anything! An int, a string, a ref or the common record type, which you should declare right before the reducerComponent
call:
type state = {count: int, show: bool};
let component = ReasonReact.reducerComponent("Example");
let make = (~onClick, _children) => {
...component,
initialState: () => {count: 0, show: true},
/* ... other fields */
};
Since the props are just the arguments on make
, feel free to read into them to initialize your state based on them.
Actions & Reducer
In ReactJS, you'd update the state inside a callback handler, e.g.
{
/* ... other fields */
handleClick: function() {
this.setState({count: this.state.count + 1});
},
handleSubmit: function() {
this.setState(...);
},
render: function() {
return (
<MyForm
onClick={this.handleClick}
onSubmit={this.handleSubmit} />
);
}
}
In ReasonReact, you'd gather all these state-setting handlers into a single place, the component's reducer
! Please refer to the first snippet of code on this page.
Note: if you ever see mentions of self.reduce
, this is the old API. The new API is called self.send
. The old API's docs are here.
A few things:
- There's a user-defined type called
action
, named so by convention. It's a variant of all the possible state transitions in your component. In state machine terminology, this'd be a "token". - A user-defined
state
type, and aninitialState
. Nothing special. - The current
state
value is accessible throughself.state
, wheneverself
is passed to you as an argument of some function. - A "reducer"! This pattern-matches on the possible actions and specifies what state update each action corresponds to. In state machine terminology, this'd be a "state transition".
- In
render
, instead ofself.handle
(which doesn't allow state updates), you'd useself.send
.send
takes an action.
So, when a click on the dialog is triggered, we "send" the Click
action to the reducer, which handles the Click
case by returning the new state that increments a counter. ReasonReact takes the state and updates the component.
Note: just like for self.handle
, sometimes you might be forwarding send
to some helper functions. Pass the whole self
instead and annotate it. This avoids a complex self
record type behavior. See Record Field send
/handle
Not Found.
State Update Through Reducer
Notice the return value of reducer
? The ReasonReact.Update
part. Instead of returning a bare new state, we ask you to return the state wrapped in this "update" variant. Here are its possible values:
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.
Important Notes
Please read through all these points, if you want to fully take advantage of reducer
and avoid future ReactJS Fiber race condition problems.
- The
action
type's variants can carry a payload:onClick=(data => self.send(Click(data.foo)))
. - Don't pass the whole event into the action variant's payload. ReactJS events are pooled; by the time you intercept the action in the
reducer
, the event's already recycled. reducer
must be pure! Aka don't do side-effects in them directly. You'll thank us when we enable the upcoming concurrent React (Fiber). UseSideEffects
orUpdateWithSideEffects
to enqueue a side-effect. The side-effect (the callback) will be executed after the state setting, but before the next render.- If you need to do e.g.
ReactEvent.BlablaEvent.preventDefault(event)
, do it inself.send
, before returning the action type. Again,reducer
must be pure. - Feel free to trigger another action in
SideEffects
andUpdateWithSideEffects
, e.g.UpdateWithSideEffects(newState, (self) => self.send(Click))
. - If your state only holds instance variables, it also means (by the convention in the instance variables section) that your component only contains
self.handle
, noself.send
. You still need to specify areducer
like so:reducer: ((), _state) => ReasonReact.NoUpdate
. Otherwise you'll get avariable cannot be generalized
type error.
Tip
Cram as much as possible into reducer
. Keep your actual callback handlers (the self.send(Foo)
part) dumb and small. This makes all your state updates & side-effects (which itself should mostly only be inside ReasonReact.SideEffects
and ReasonReact.UpdateWithSideEffects
) much easier to scan through. Also more ReactJS fiber async-mode resilient.
Async State Setting
In ReactJS, you could use setState
inside a callback, like so:
setInterval(() => this.setState(...), 1000);
In ReasonReact, you'd do something similar:
Js.Global.setInterval(() => self.send(Tick), 1000)