= ReasonReact.statelessComponent("Greeting"); let make = (~name, _children) %=> { ''...component, !/* spread the template's other defaults into here #*/ render: _self %=> <div> {ReasonReact.string(name)} )</div> };
int, }; /* Action declaration */ type action = | Click; /* 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}, /* State transitions */ reducer: (action, state) => switch (action) { | Click => ReasonReact.Update({count: state.count + 1}) }, 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> </div>; }, };
React.createElement(component.jsClass) }; (simplified approximation) !/* still in Greeting.re #*/ let component = ReasonReact.statelessComponent("Greeting"); let make = (~name, _children) %=> { ''...component, !/* spread the template's other defaults into here render: _self %=> <div> {ReasonReact.string(name)} )</div> };
'state, willUnmount: self('state, 'action) %=> unit, didUpdate: oldNewSelf('state, 'action) %=> unit, shouldUpdate: oldNewSelf('state, 'action) %=> bool, willUpdate: oldNewSelf('state, 'action) %=> unit, didMount: self('state, 'action) %=> unit, initialState: unit %=> 'initialState, reducer: ('action, 'state) %=> update('state, 'action), render: self('state, 'action) %=> React.element, } and update('state, 'action) = | NoUpdate | Update('state) | SideEffects(self('state, 'action) %=> unit) | UpdateWithSideEffects('state, self('state, 'action) %=> unit) and self('state, 'action) = { handle: 'payload. (('payload, self('state, 'action)) %=> unit, 'payload) %=> unit, state: 'state, send: 'action %=> unit, onUnmount: (unit %=> unit) %=> unit, } and oldNewSelf('state, 'action) = { oldSelf: self('state, 'action), newSelf: self('state, 'action), }; 11/** This is not exposed, only used internally so that useReducer can return side-effects to run later. #*/ type fullState('state, 'action) = { sideEffects: ref(array(self('state, 'action) %=> unit)), state: ref('state), }; let useRecordApi = componentSpec %=> { open React.Ref; let initialState = React.useMemo0(componentSpec.initialState); let unmountSideEffects = React.useRef(['||]); let ({state, sideEffects}, send) = React.useReducer( (fullState, action) %=> 11/** Keep fullState.state in a ref so that willReceiveProps can alter it. It's the only place we let it be altered. Keep fullState.sideEffects so that they can be cleaned without a state update. It's important that the reducer only **creates new refs** an doesn't alter them, otherwise React wouldn't be able to rollback state in concurrent mode. #*/ ( switch (componentSpec.reducer(action, fullState.state^)) { | NoUpdate %=> fullState !/* useReducer returns of the same value will not rerender the component #*/ | Update(state) %=> {''...fullState, state: ref(state)} | SideEffects(sideEffect) %=> { ''...fullState, sideEffects: ref( Js.Array.concat(fullState.sideEffects^, [|sideEffect|]), ), } | UpdateWithSideEffects(state, sideEffect) %=> { sideEffects: ref( Js.Array.concat(fullState.sideEffects^, [|sideEffect|]), ), state: ref(state), } } ), {sideEffects: ref(['||]), state: ref(initialState)}, ); 11/** This is the temp self for willReceiveProps #*/ let rec self = { handle: (fn, payload) %=> fn(payload, self), send, state: state^, onUnmount: sideEffect %=> Js.Array.push(sideEffect, unmountSideEffects)->current))->ignore, }; let upToDateSelf = React.useRef(self); let hasBeenCalled = React.useRef(false); 11/** There might be some potential issues with willReceiveProps, treat it as it if was getDerivedStateFromProps. #*/ state .:= componentSpec.willReceiveProps(self); let self = { handle: (fn, payload) %=> fn(payload, upToDateSelf)->current), send, state: state^, onUnmount: sideEffect %=> Js.Array.push(sideEffect, unmountSideEffects)->current))->ignore, }; let oldSelf = React.useRef(self); let _mountUnmountEffect = React.useEffect0(() %=> { componentSpec.didMount(self); Some( () %=> { Js.Array.forEach(fn %=> fn(), unmountSideEffects)->current); !/* shouldn't be needed but like - better safe than sorry? #*/ unmountSideEffects)->setCurrent(['||]); componentSpec.willUnmount(upToDateSelf)->React.Ref.current); }, ); }); let _didUpdateEffect = React.useEffect(() %=> { if (hasBeenCalled)->current) { componentSpec.didUpdate({oldSelf: oldSelf)->current, newSelf: self}); } else { hasBeenCalled)->setCurrent(true); }; oldSelf)->setCurrent(self); None; }); 11/** Because sideEffects are only added through a **new** ref, we can use the ref itself as the dependency. This way the effect doesn't re-run after a cleanup. #*/ React.useEffect1( () %=> { if (Js.Array.length(sideEffects^) > 0) { Js.Array.forEach(func %=> func(self), sideEffects^); sideEffects .:= ['||]; }; None; }, [|sideEffects|], ); let mostRecentAllowedRender = React.useRef(React.useMemo0(() %=> componentSpec.render(self))); upToDateSelf)->setCurrent(self); if (hasBeenCalled)->current 1&& componentSpec.shouldUpdate({ oldSelf: oldSelf)->current, newSelf: self, })) { componentSpec.willUpdate({oldSelf: oldSelf)->current, newSelf: self}); mostRecentAllowedRender)->setCurrent(componentSpec.render(self)); }; mostRecentAllowedRender)->current; };
'state, willUnmount: self('state, 'action) %=> unit, didUpdate: oldNewSelf('state, 'action) %=> unit, shouldUpdate: oldNewSelf('state, 'action) %=> bool, willUpdate: oldNewSelf('state, 'action) %=> unit, didMount: self('state, 'action) %=> unit, initialState: unit %=> 'initialState, reducer: ('action, 'state) %=> update('state, 'action), render: self('state, 'action) %=> React.element, } and update('state, 'action) = | NoUpdate | Update('state) | SideEffects(self('state, 'action) %=> unit) | UpdateWithSideEffects('state, self('state, 'action) %=> unit) and self('state, 'action) = { handle: 'payload. (('payload, self('state, 'action)) %=> unit, 'payload) %=> unit, state: 'state, send: 'action %=> unit, onUnmount: (unit %=> unit) %=> unit, } and oldNewSelf('state, 'action) = { oldSelf: self('state, 'action), newSelf: self('state, 'action), }; 11/** This is not exposed, only used internally so that useReducer can return side-effects to run later. #*/ type fullState('state, 'action) = { sideEffects: ref(array(self('state, 'action) %=> unit)), state: ref('state), }; let useRecordApi = componentSpec %=> { open React.Ref; let initialState = React.useMemo0(componentSpec.initialState); let unmountSideEffects = React.useRef(['||]); let ({state, sideEffects}, send) = React.useReducer( (fullState, action) %=> 11/** Keep fullState.state in a ref so that willReceiveProps can alter it. It's the only place we let it be altered. Keep fullState.sideEffects so that they can be cleaned without a state update. It's important that the reducer only **creates new refs** an doesn't alter them, otherwise React wouldn't be able to rollback state in concurrent mode. #*/ ( switch (componentSpec.reducer(action, fullState.state^)) { | NoUpdate %=> fullState !/* useReducer returns of the same value will not rerender the component #*/ | Update(state) %=> {''...fullState, state: ref(state)} | SideEffects(sideEffect) %=> { ''...fullState, sideEffects: ref( Js.Array.concat(fullState.sideEffects^, [|sideEffect|]), ), } | UpdateWithSideEffects(state, sideEffect) %=> { sideEffects: ref( Js.Array.concat(fullState.sideEffects^, [|sideEffect|]), ), state: ref(state), } } ), {sideEffects: ref(['||]), state: ref(initialState)}, ); 11/** This is the temp self for willReceiveProps #*/ let rec self = { handle: (fn, payload) %=> fn(payload, self), send, state: state^, onUnmount: sideEffect %=> Js.Array.push(sideEffect, unmountSideEffects)->current))->ignore, }; let upToDateSelf = React.useRef(self); let hasBeenCalled = React.useRef(false); 11/** There might be some potential issues with willReceiveProps, treat it as it if was getDerivedStateFromProps. #*/ state .:= componentSpec.willReceiveProps(self); let self = { handle: (fn, payload) %=> fn(payload, upToDateSelf)->current), send, state: state^, onUnmount: sideEffect %=> Js.Array.push(sideEffect, unmountSideEffects)->current))->ignore, }; let oldSelf = React.useRef(self); let _mountUnmountEffect = React.useEffect0(() %=> { componentSpec.didMount(self); Some( () %=> { Js.Array.forEach(fn %=> fn(), unmountSideEffects)->current); !/* shouldn't be needed but like - better safe than sorry? #*/ unmountSideEffects)->setCurrent(['||]); componentSpec.willUnmount(upToDateSelf)->React.Ref.current); }, ); }); let _didUpdateEffect = React.useEffect(() %=> { if (hasBeenCalled)->current) { componentSpec.didUpdate({oldSelf: oldSelf)->current, newSelf: self}); } else { hasBeenCalled)->setCurrent(true); }; oldSelf)->setCurrent(self); None; }); 11/** Because sideEffects are only added through a **new** ref, we can use the ref itself as the dependency. This way the effect doesn't re-run after a cleanup. #*/ React.useEffect1( () %=> { if (Js.Array.length(sideEffects^) > 0) { Js.Array.forEach(func %=> func(self), sideEffects^); sideEffects .:= ['||]; }; None; }, [|sideEffects|], ); let mostRecentAllowedRender = React.useRef(React.useMemo0(() %=> componentSpec.render(self))); upToDateSelf)->setCurrent(self); if (hasBeenCalled)->current 1&& componentSpec.shouldUpdate({ oldSelf: oldSelf)->current, newSelf: self, })) { componentSpec.willUpdate({oldSelf: oldSelf)->current, newSelf: self}); mostRecentAllowedRender)->setCurrent(componentSpec.render(self)); }; mostRecentAllowedRender)->current; }; Slightly more complex
rec self = { handle: (fn, payload) %=> fn(payload, self), send, state: state^, onUnmount: sideEffect %=> Js.Array.push(sideEffect, unmountSideEffects)->current))->ignore, }; let upToDateSelf = React.useRef(self); let hasBeenCalled = React.useRef(false); 11/** There might be some potential issues with willReceiveProps, treat it as it if was getDerivedStateFromProps. #*/ state .:= componentSpec.willReceiveProps(self);
oldSelf)->current, newSelf: self}); } else { hasBeenCalled)->setCurrent(true); }; oldSelf)->setCurrent(self); None; }); 11/** Because sideEffects are only added through a **new** ref, we can use the ref itself as the dependency. This way the effect doesn't re-run after a cleanup. #*/ React.useEffect1( () %=> { if (Js.Array.length(sideEffects^) > 0) { Js.Array.forEach(func %=> func(self), sideEffects^); sideEffects .:= ['||];
Stateless with other lifecycle → useRecordApi Stateful → useReducer Stateful with mount → useReducer with useEffect Stateful with lifecycle → useRecordApi
- Take component function arguments - Take `render` body from the record - Take didMount body - Create a function with arguments, a useEffect containing didMount body, then render body
component function arguments - Take `render` body from the record - Take `reducer` body from the record - Create a function with arguments, a ReactUpdate.useReducer containing the reducer body and render body
didMount - Take component function arguments - Take `render` body from the record - Take `reducer` body from the record - Take didMount body - Create a function with arguments, a ReactUpdate.useReducer containing the reducer body, a useEffect containing didMount body and render body