UNPKG

reason-react-brunch

Version:

React bindings for Reason, modified to work with brunch and bucklescript

158 lines (117 loc) 7.4 kB
--- title: 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](http://redux.js.org)) built in. If that word doesn't mean anything to you, just think of it as a state machine. If _that_ word doesn't mean anything 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. ```reason /* 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**: ```reason 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. ```javascript { /* ... 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](https://github.com/reasonml/reason-react/blob/e17fcb5d27a2b7fb2cfdc09d46f0b4cf765e50e4/docs/state-actions-reducer.md). 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 an `initialState`. Nothing special. - The current `state` value is accessible through `self.state`, whenever `self` is passed to you as an argument of some function. - A "**reducer**"! This [pattern-matches](https://reasonml.github.io/docs/en/pattern-matching.html) on the possible actions and specify what state update each action corresponds to. _In state machine terminology, this'd be a "state transition"_. - In `render`, instead of `self.handle` (which doesn't allow state updates), you'd use `self.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 increment 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](record-field-send-handle-not-found.md). ## 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). Use `SideEffects` or `UpdateWithSideEffects` 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. `ReactEventRe.BlablaEvent.preventDefault(event)`, do it in `self.send`, before returning the action type. Again, `reducer` must be pure. - Feel free to trigger another action in `SideEffects` and `UpdateWithSideEffects`, 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`, no `self.send`. You still needs to specify a `reducer` like so: `reducer: ((), _state) => ReasonReact.NoUpdate`. Otherwise you'll get a `variable 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: ```reason Js.Global.setInterval(() => self.send(Tick), 1000) ```