DEV Community

Vlad Oganov
Vlad Oganov

Posted on

useReducer for the win

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. 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:

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:

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:

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 ๐Ÿš€ And this is a nice practice in your toolkit for creating such flows with conditional showing of steps. Be happy.

Cheers!

Top comments (0)