Hi
Lately I have been using too much useEffect, its becoming hard to track the state. After seeing David Piano (author of XState) I have been thinking on how to do it in Rescript.
I have been using React.useReducer quite extensively. I have come up with a hook which combines React.useReducer and React.useEffect named it un-intuitively as useStateMachine.
Let's see how useStateMachine is
- structured
- how to structure your state
- how to use it in the project
- how to trigger side effects as well as part of the state actions
State and actions
type effectulActions =
| #Network(networkReducer)
| #Db(dbReducer)
| #Storage(storageReducer)
actions with some tags(network, database, storage) would trigger the functions in the useEffect
If the actions are part of this then trigger the effectful action in the useEffect
Thus shielding the complexity from the react component
How the dependencies should be added to the useEffect ?
How the state should be part of the useEffect dependencies ?
The above are the outstanding questions for now.
For starters we can add the whole state to the dependencies.
How the action should translate to sideEffect triggering function call ?
How it should reflect/update in the state ?
This is a basic structure I could come up with.
let reducer = (state, action) => {
switch action {
| #Network(networkReducer) => {
networkReducer(state, action)
}
}
let sideEffectReducer = (state, action) => {
}
useStateMachine hook
type stateMachineState<'state, 'effectfulAction> = {
state: 'state,
effectfulAction: option<'effectfulAction>,
}
type stateMachineActions<'state, 'action, 'effectfulAction> =
| StateWithoutSideEffect('action)
| StateAfterSideEffect(stateMachineState<'state, 'effectfulAction>)
type reducerType<'state, 'action, 'effectfulAction> = (
stateMachineState<'state, 'effectfulAction>,
stateMachineActions<'state, 'action, 'effectfulAction>,
) => stateMachineState<'state, 'effectfulAction>
type sideEffectReducerType<'state, 'effectfulAction> = (
stateMachineState<'state, 'effectfulAction>,
'effectfulAction,
) => Js.Promise.t<stateMachineState<'state, 'effectfulAction>>
let useStateMachine = (
reducer: reducerType<'state, 'action, 'effectfulAction>,
sideEffectReducer: sideEffectReducerType<'state, 'effectfulAction>,
initialState: stateMachineState<'state, 'effectfulAction>,
) => {
let (stateMachineState, dispatch) = React.useReducer(reducer, initialState)
// This does not handle the clean up
React.useEffect3(() => {
switch stateMachineState.effectfulAction {
| Some(effectfulAction) => {
open Promise
sideEffectReducer(stateMachineState, effectfulAction)
->then(newState => {
dispatch(StateAfterSideEffect(newState))
resolve()
})
->catch(error => {
Js.log2("error", error)
resolve()
})
->ignore
}
| None => ()
}
None
}, (sideEffectReducer, stateMachineState, dispatch))
(stateMachineState.state, dispatch)
}
A lot going on in the above code. Lets break down
1.
type stateMachineState<'state, 'effectfulAction> = {
state: 'state,
effectfulAction: option<'effectfulAction>,
}
stateMachine state type is parameterised over state type and effectfulAction which is optional. The component using this state would pass the state and type of the sideEffectul actions it intends to deal with.
2.
type stateMachineActions<'state, 'action, 'effectfulAction> =
| StateWithoutSideEffect('action)
| StateAfterSideEffect(stateMachineState<'state, 'effectfulAction>)
stateMachine actions are parameterised our the state, action and effectulActions. There are two states this statemachine could be in (this was the simplest I could make, there could be more extensions for now this would suffice).
- Whenever there are an action dispatch then StateWithoutSideEffect action is dispatched in the reducer case could decide whether there could be any effectulActions that needs to be done based on the given case. If so then state.effectulAction is populated else it is None.
- useStateMachine hook explanation. Whenever there effectulAction is populated then sideEffectReducer is invoke with appropriate parameters inside the useEffect hook. Thus all sideEffects happen in the sideEffectReducer inside the useEffect. Thus we could keep our code clean of the useEffect as much as possible.
Usage of the useStateMachine
type action = INFO(array<string>)
type state = {options: array<string>}
type rec sideEffectActions =
INFO_UPDATE(StateMachine.stateMachineState<state, sideEffectActions>, string)
let getInitialState = () => {
let state = {
options: [],
}
let stateMachineState: StateMachine.stateMachineState<state, sideEffectActions> = {
state,
effectfulAction: None,
}
stateMachineState
}
let reducer = (state, action) => {
switch action {
| INFO(_) => state
}
}
let sideEffectReducer = (
state: StateMachine.stateMachineState<state, sideEffectActions>,
action: sideEffectActions,
): Js.Promise.t<StateMachine.stateMachineState<state, sideEffectActions>> => {
open Promise
switch action {
| INFO_UPDATE(_state, _str) =>
// side effectul action
resolve(state)
}
}
let stateMachineReducer = (
state: StateMachine.stateMachineState<state, sideEffectActions>,
action: StateMachine.stateMachineActions<state, action, sideEffectActions>,
): StateMachine.stateMachineState<'state, 'effectfulAction> => {
switch action {
| StateWithoutSideEffect(action) =>
switch action {
| INFO(_options) => // every state change should trigger the debounce api call
{
...state,
effectfulAction: Some(INFO_UPDATE(state, "asd")),
}
}
| _ => state
}
}
Top comments (0)