DEV Community

Srikanth Kyatham
Srikanth Kyatham

Posted on • Edited on

Rescript React useStateMachine without useEffect usage

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

  1. structured
  2. how to structure your state
  3. how to use it in the project
  4. how to trigger side effects as well as part of the state actions

State and actions

type effectulActions = 
| #Network(networkReducer)
| #Db(dbReducer)
| #Storage(storageReducer)

Enter fullscreen mode Exit fullscreen mode

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) => {
  }
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

A lot going on in the above code. Lets break down

1.

type stateMachineState<'state, 'effectfulAction> = {
  state: 'state,
  effectfulAction: option<'effectfulAction>,
}
Enter fullscreen mode Exit fullscreen mode

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>)
Enter fullscreen mode Exit fullscreen mode

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.
  1. 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
  }
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)