useReducer for the win
Tags: react
Published: 2020-05-25 18:42:58

Hey, how are you there? Well, here is a story. It's quite small, but it can save your time and health. So keep reading.

We wanted to have a sequence of steps in our application which it’s changed depending on user’s answers. Take a look:
```
step with yes/no question -> if yes: Step 1 -> if yes: Step 2 -> Step 3 -> Step 4
                          -> if no: skip    -> if no:  skip   -> Step 3 -> Step 4
```

The logic is the following:
1. User pick an answer in a form
2. The form sends the data to an API – the API persists the answer
3. On success we change the state of the redux store
4. We change a flow of steps depending on the answers
5. Go to the next step accordingly to the flow
6. Profit

Disclaimer 1.: there is a pretty nice library that can help to manage sophisticated flows – [xstate](https://xstate.js.org). And for this case it'd be an overkill, so we created our small, simple, homemade solution 😌

Disclaimer 2.: the code presented here is simplified to focus on the issue. Please, don't judge 

And here is the code:
```javascript
function useSteps(flow) {
  const [step, setStep] = useState(_.first(flow))

  const goBack = () => {
    const prevStep = _.nth(flow, flow.indexOf(step) - 1)

    setStep(prevStep)
  }

  const goForward = () => {
    const nextStep = _.nth(flow, flow.indexOf(step) + 1)

    setStep(nextStep)
  }

  return { current: step, goForward, goBack }
}

function LeComponent() {
  const entity = useEntity()

  const flow = [
    STEP_1,
    entity.yesOrNo === 'Yes' && STEP_2,
    entity.yesOrNo === 'Yes' && STEP_3,
    STEP_4,
  ].filter(Boolean)

  const steps = useSteps(flow)

  return pug`
    if steps.current === STEP_1
       LeForm(
          onCancel=steps.goBack
          onSubmitSuccess=steps.goForward
        )

    if steps.current === STEP_2
       .........
  `
}
```

And it won't work. Every time we run it, `onSubmitSuccess` is called with the old `steps.goForward` so even if user answered 'yes', we redirect them to `Step 3`. Meh. Worth to mention: the `entity` and the `flow` are updated correctly before the action of going forward. It. Must. Work. Except it doesn't.

Ok, an overengineered solution to help. Every time user updates the value in the form we update the state of the parent component using `redux-form`'s `onChange`. Also we have to sync the state of our component with the state that has been persisted on the API in case of the page reloading – so we have this `useEffect` there. Shit is getting crazy. Take a look:
```javascript
function LeComponent() {
  const entity = useEntity()

  const [yesOrNo, setYesOrNo] = useState(null)
  const handleYesOrNo = formData => setYesOrNo(formData.yesOrNo)

  useEffect(() => {
    setYesOrNo(entity.yesOrNo)
  }, [entity.yesOrNo])

  const flow = [
    STEP_1,
    entity.yesOrNo === 'Yes' && STEP_2,
    entity.yesOrNo === 'Yes' && STEP_3,
    STEP_4,
  ].filter(Boolean)

  const steps = useSteps(flow)

  return pug`
    if steps.current === STEP_1
       LeForm(
          onCancel=steps.goBack
          onSubmitSuccess=steps.goForward
          onChange=handleYesOrNo
        )

    if steps.current === STEP_2
       .........
  `
}
```

Perfect! I'm being paid for a reason definitely. But no, come on, we can't leave it as that. What if we need to track more answers? So we started to investigate if there is something wrong with `redux-form`. Every value around is new, but `onSubmitSuccess` is living in the past.

And we didn't find what really happened. Instead we decided why not to use `useReducer` in the `useSteps`. How? Take a look:
```javascript
function useSteps(flow) {
  function reducer(step, action) {
    switch (action.type) {
      case 'goBack':
        return _.nth(flow, flow.indexOf(step) - 1)
      case 'goForward':
        return _.nth(flow, flow.indexOf(step) + 1)
      default:
        return step
    }
  }

  const [current, dispatch] = useReducer(reducer, _.first(flow))

  const goBack = () => dispatch({ type: 'goBack' })

  const goForward = () => dispatch({ type: 'goForward' })

  return { current, goForward, goBack }
}
```

Sweet! Now `goForward` just push an action w/o rely on the closure, so we can remove all of these stuff of keeping state of the answer in the component and make it in the _react way_ so to say.

And it worked out :rocket: And this is a nice practice in your toolkit for creating such flows with conditional showing of steps. Be happy. 

Cheers!