skip to Main Content

This simple counter button does work as expected. Initially set to zero, its label is incremented each time I click on it:

function Counter() {
    const INCREMENT = 'INC';
    const [state, dispatch] = React.useReducer(reducer, { counter: 0 });

    function reducer(state, action) {
        switch (action.type) {
            case INCREMENT:
                return { counter: state.counter + 1 };
            default:
                throw new Error("Unknown action!");
        }
    }

    return <button onClick={() => dispatch({ type: INCREMENT })}>{state.counter}</button>;
}

However, if I use Symbol for the INCREMENT constant, I get "Unknown action!" each time I click on it:

function Counter() {
    const INCREMENT = Symbol();
    const [state, dispatch] = React.useReducer(reducer, { counter: 0 });

    function reducer(state, action) {
        switch (action.type) {
            case INCREMENT:
                return { counter: state.counter + 1 };
            default:
                throw new Error("Unknown action!");
        }
    }

    return <button onClick={() => dispatch({ type: INCREMENT })}>{state.counter}</button>;
}

Can you explain this behaviour?

2

Answers


  1. You’re comparing two different Symbols. The INCREMENT used within your reducer is the INCREMENT symbol created for the current render (as you’re redefining reducer() within your component, which is not typically done), whereas the action.type within your reducer is the symbol created for the previous render. That’s because when you call dispatch(action):

    • Your component executes again, creating a fresh unique INCREMENT symbol. As you can’t create the same Symbol twice with Symbol(), this is unique.
    • Your function reducer() is redeclared, allowing it to access the new unique symbol just created.
    • The line const [state, dispatch] = React.useReducer(reducer, { counter: 0 }); executes, triggering the reducer function to execute with action as an argument. Here action is your object from the previous render that has the old INCREMENT symbol.

    As your new symbol INCREMENT is different from the old one action.type, they’ll have different reference identities when compared in the switch.

    Instead, you can create the Symbol outside of your component so that the symbol remains consistent across rerenders:

    const INCREMENT = Symbol();
    function Counter() {
        const [state, dispatch] = React.useReducer(reducer, { counter: 0 });
    
        function reducer(state, action) {
            switch (action.type) {
                case INCREMENT:
                    return { counter: state.counter + 1 };
                default:
                    throw new Error("Unknown action!");
            }
        }
    
        return <button onClick={() => dispatch({ type: INCREMENT })}>{state.counter}</button>;
    }
    
    ReactDOM.createRoot(document.body).render(<Counter />);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>

    Alternatively, you can use Symbol.for("INCREMENT") which creates a "shared" symbol, but as Bergi points out, this doesn’t have many benefits over just using a string:

    function Counter() {
        const INCREMENT = Symbol.for("INCREMENT");
        const [state, dispatch] = React.useReducer(reducer, { counter: 0 });
    
        function reducer(state, action) {
            switch (action.type) {
                case INCREMENT:
                    return { counter: state.counter + 1 };
                default:
                    throw new Error("Unknown action!");
            }
        }
    
        return <button onClick={() => dispatch({ type: INCREMENT })}>{state.counter}</button>;
    }
    
    ReactDOM.createRoot(document.body).render(<Counter />);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
    Login or Signup to reply.
  2. The problem is that const INCREMENT = Symbol(); creates a new symbol on every render of your Counter component. Define the reducer and the action type symbols outside of the component:

    const INCREMENT = Symbol();
    function reducer(state, action) {
        switch (action.type) {
            case INCREMENT:
                return { counter: state.counter + 1 };
            default:
                throw new Error("Unknown action!");
        }
    }
    const initial = { counter: 0 };
    
    function Counter() {
        const [state, dispatch] = React.useReducer(reducer, initial);    
        return <button onClick={() => dispatch({ type: INCREMENT })}>{state.counter}</button>;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search