How To Refactor Your Components with React’s useState

I think most people originally learn React by writing class components as opposed to functional ones. That was definitely my experience, and it took me a while to understand the full benefit of hooks. In particular, it took me a while to understand that with useState, state didn’t necessarily have to be an object any more.

In class based components, state is virtually always an object or an array. If you’re making a really simple component, like a counter or text input, it’s possible that state could be a string or a number. But really those are more examples than actual use cases. In a real app you’re supposed to lift state up. This means that any stateful component will likely have many bits of information being fed to it from various child components, and an object is the most natural way to organize these.

Starting to Use Hooks

So when I started using hooks, I tended to follow the same pattern. I would initialize state as an object and then have various handlers update its properties. But unlike class components, functional components can have unlimited stateful variables by utilizing hooks. For example, here’s the state for a component that manages a form used for ordering pizza. First as a class-based component:

class PizzaForm extends React.Component {
    state = {
           toppings: ['cheese'],
           size: 'large'
    }
  ...
}

Then as a functional component with useState:

function PizzaForm(){
    let [toppings, updateToppings] = useState(['cheese']);
    let [size, updateSize] = useState('large');
    /*...*/
}

In the functional example, “toppings” and “size” are both stateful values that persist between re-renders, but they’re also separate values, not part of the same overarching state object.

The Benefits

Why is this a good thing? For one thing, in many cases it allows you to skip writing separate handler functions to update state. An example of this would be a React Native TextInput component. It also applies to other component that passes the new state value directly into its on* method.

let [name, setName] = useState( 'Peter' ); 
<TextInput value={name} onChangeText={setName} />

Second, it helps minimize the amount of spreading and/or Object.assign()-ing your code requires. Returning to the example of an order form, suppose that form’s state now looks like this:

const initialState = {
    order: {toppings: [], size: 'large', deliver: true},
    customer: {name: 'Mark', address: '1 Main St'}
}

let [pizzaState, updatePizzaState] = useState( initialState );

As you can see, we’ve split the order information and customer information into separate properties. This helps keep things organized. The only problem is, whichever method we choose to update this object is going to be a real pain. The following method is one I came up with before I learned my lesson about splitting state up. It’s long and won’t even work for a property defined directly on the pizzaState object.

const handleChangeForm = outerProp => innerProp => value => {
    updateState({
        ...state, 
        [outerProp]:{
            ...state[outerProp],
            [innerProp]: value, 
        }
    });
}

Some guidance from the React Docs that I should have read more closely

Situations like this are when splitting state up really comes in handy. It’s a simpler way of updating just the value you need to update, and leaving everything else alone. If you need to package the separate state variables into an object later on it’s easy enough to do that.

function PizzaForm ({updateCustomer, submitOrder}){
    const [toppings, updateToppings] = useState(['cheese']),
        [size, updateSize] = useState('large'),
        [delivery, updateDelivery] = useState( false ),
        [name, updateName] = useState('Mark'),
        [address, updateAddress = useState( '1 Main St.' );

    const handleSubmitForm = () => {
        /* Submit the customer information. */
        updateCustomer({ name, address });
        /*process the order */
        submitOrder({ toppings, size, delivery, address })
    }

    return(
        /* A form with a bunch of inputs */
    )
}

Abstracting State

You could argue that writing out all those state variables and updaters is still tedious, even if it’s an improvement. We can make it more concise by using a custom hook to share update logic between state variables. For instance, address and name would both be some kind of text input. We could change the ‘delivery’ variable from a boolean to a string. A <select> or picker-type control could update both the size and delivery variables. Here’s an example of a custom hook that could handle state for both the delivery and size variables:

const usePickerInput = ( initialValue ) => {
    const [ selected, updateSelected]  = useState( initialValue );

    return {
        value: selected,
        onChange: updateSelected( value )
    }
}

Let’s imagine we used the same process to make a useTextInput hook. Now, our whole component could look something like this. Overall, it’s pretty lean. State lives in our custom hooks, which manage it in the background. The component itself is mostly just assignments some very simple JSX.

function PizzaForm ({updateCustomer, submitOrder}){
    const [toppings, updateToppings] = useState(['cheese']),
          size = usePickerInput( 'large' ),
          delivery = usePickerInput( 'delivery' ),
          name = useTextInput( 'Mark' ),
          address  = useTextInput( '1 Main Street' );

    const handleSubmitForm = () => {
        updateCustomer({ name, address });
        submitOrder({ toppings, size, deliver, address })
    }
return(
        <Form>
            <CheckboxGroup checked={toppings} onChange={updateToppings} />
            <Picker {...size} />
            <Picker {...delivery} />
            <TextInput {...name} />
            <TextInput {...address} />
            <Button onPress={handleSubmitForm} />
       </Form>
    )
}

Conclusion

When I first encountered the idea of breaking state up into different variables, rather than a single object, I had reservations. The idea of “data flowing downward” from an omniscient hub at the top (ie, state) had been central to the way I made sense of React in the first place. If an app was supposed to have a single source of truth, you couldn’t break that source of truth into little pieces. I still feel like splitting state is a mild violation. But its practical benefits win out in the end.