Dev-2019
Dev-2019

Reputation: 557

state.map is not a function at the very moment of dispatch

I have a react component which uses useReducer hook to handle dynamic Text field addition and removal. expected working is as follows,

  1. type 'add' will append current state with one more empty object. (onClick event of add button)
  2. type 'remove' will remove the object from state array. (onClick event of delete button)
  3. fields are rendered using map function on state variable.

but when I try to dispatch with add/remove the state.map function will goes undefined. any sort of helps are really appreciated

code snippet.

export default function Create(props) {
    const initialState = [{}];

    function reducer(state, action) {
        switch (action.type) {
            case 'add':
                state = {...state}
                console.log(state)
                return state;
            case 'remove':
                return state;
            default:
                throw new Error();
        }
    }

    const {dialogOpen, data} = {...props}
    const [open, setOpen] = React.useState(false);
    const [state, dispatch] = React.useReducer(reducer, initialState)


    return (
        <div>
                    {
                        state.map(() => {
                            return (
                                <div style={{display: "flex", alignItems: "center"}}>
                                    <ApplicationBudget/>
                                    <IconButton style={{marginLeft: 10}} onClick={() => dispatch({type: 'remove'})}
                                                aria-label="delete" margin={{margin: 2}}>
                                        <DeleteOutlined/>
                                    </IconButton>
                                    <IconButton aria-label="add" onClick={() => dispatch({type: 'add'})}
                                                margin={{margin: 1}}>
                                        <Add/>
                                    </IconButton>
                                </div>
                            )
                        })
                    }

        </div>
    );
}

Upvotes: 0

Views: 205

Answers (2)

Dev-2019
Dev-2019

Reputation: 557

Thanks fjc for your help. end up with below solution

export default function Create(props) {
    const initialState = [{}];

    function reducer(state, action) {
        switch (action.type) {
            case 'add':
                return [...state, ...action.payload]
            case 'remove':
                let newState = [...state]
                newState.splice(action.payload, 1)
                return newState
            default:
                throw new Error();
        }
    }

    const {dialogOpen, data} = {...props}
    const [open, setOpen] = React.useState(false);
    const [state, dispatch] = React.useReducer(reducer, initialState)

    return (
        <div>
                    {

                        state.map((budget, index) => {
                            return (
                                <div key={index} style={{display: "flex", alignItems: "center"}}>
                                    <ApplicationBudget/>
                                    <IconButton style={{marginLeft: 10}}
                                                onClick={() => dispatch({type: 'remove', payload: index })}
                                                aria-label="delete" margin={{margin: 2}}>
                                        <DeleteOutlined/>
                                    </IconButton>
                                    <IconButton aria-label="add"
                                                onClick={() => dispatch({type: 'add', payload: [{}]})}
                                                margin={{margin: 1}}>
                                        <Add/>
                                    </IconButton>
                                </div>
                            )
                        })


                    }
        </div>
    );
}

Upvotes: 0

fjc
fjc

Reputation: 5805

Your initial state is an array:

initialState = [{}]

So state.map works (as the prototype of Array has a map method).

However, in your reducer, you return an object:

state = {...state}

Your object has no map method.

There are two issues here:

  1. You should not modify the state variable, but leave it untouched and return a new state instead. A common way to work like this is to spread the current state into the returned state.
  2. You need to keep the type stable, so return an Array, not an Object.

Example:

 function reducer(state, action) {
        switch (action.type) {
            case 'add':
                return [...state, "new entry"];
        }
    }

Update: I added a minimal example for you. It has a reducer with two actions. One with the same faulty behavior as yours, one that's working.

function App() {
  const reducer = (state, action) => {
    switch (action.type) {
      case "add-broken": {
        return {...state};
      }
      case "add-working": {
        return [...state, ...action.payload];
      }
      default: {
        throw new Error();
      }
    }
  };
  const [state, dispatch] = React.useReducer(reducer, ['a', 'b']);
  return (
    <div>
    <ul>
    {state.map(s => <li>{s}</li>)}
    </ul>
    <button onClick={() => dispatch({type: 'add-broken', payload: 'C'})}>Add (broken)</button>
    <button onClick={() => dispatch({type: 'add-working', payload: 'C'})}>Add (working)</button>
    </div>
  );
}
ReactDOM.render(<App />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="app"></div>

Upvotes: 1

Related Questions