Search code examples
javascriptreactjsreduxredux-toolkit

Redux Toolkit, trying to use filter in a reducer function, but it doesn't behave as expected


I have a reducer that should filter the array and update its state


searchByName: (state, action) => {

      state.users = state.users.filter((user) =>
      user.name.toLowerCase().includes(action.payload.toLowerCase())
    ); 
    },

This is to include a search bar functionality in the SPA. However, this way it only works if I type in the search bar, on using backspace to delete it stops working. I tried reading different docs and resources such as https://redux.js.org/introduction/getting-started#redux-toolkit-example to understand what's happening and modify the code to use return but nothing seems to work?

if I use it this way, as was suggested in some answers

searchByName: (state, action) => {

      return state.users.filter((user) =>
      user.name.toLowerCase().includes(action.payload.toLowerCase())
    ); 
    },

I get Uncaught (in promise) Error: [Immer] Immer only supports setting array indices and the 'length' propert error

Also if I use map or sort it does work this way, I can't understand why filter doesn't. How can I fix this?

EDIT

const slice = createSlice({
  name: "users",
  initialState: {
    users: [],
    isLoading: true,
    search: "",
  },
  reducers: {
    usersSuccess: (state, action) => {
      state.users = action.payload;
      state.isLoading = false;
    searchByName: (state, action) => {

      return {
        ...state.users,
        users: [...state.users].filter((user) => user.name.toLowerCase().includes(action.payload.toLowerCase()))
     };

    },
  },
});

EDIT 2

const dispatch = useDispatch();
  const { users, isLoading } = useSelector((state) => state.users);
  const [searchTerm, setSearchTerm] = useState("");

  const changeSearchTerm = (e) => {
    setSearchTerm(e.target.value);
  };

  useEffect(() => {
    dispatch(userActions.searchByName(searchTerm));
    console.log(searchTerm);
  }, [searchTerm]);

  return (
    <div>
      <input onChange={changeSearchTerm} type="text" value={searchTerm}></input>
    </div>
  );
};

EDIT 3

I added the state filteredUsers in slice

initialState: {
    users: [],
    filteredUsers: [],
    isLoading: true,
    search: "",
  },

Then in the component that is supposed to display the users I changed to

const { filteredUsers, isLoading } = useSelector((state) => state.filteredUsers);

And then mapped it

{filteredUsers.map((user) => (
            <Users user={user} />
          ))}

Solution

  • A reducer function always returns the full state. Reducers use Immer internally to help manage immutability and write good state changing code.

    Here's how you should actually return in searchByName:

    searchByName: (state, action) => {
        // The object you return is the full state object update in your reducer
        return {
           ...state,
           users: [...state.users].filter((user) => user.name.toLowerCase().includes(action.payload.toLowerCase())
        };
    },
    

    Edit: https://codesandbox.io/s/elated-estrela-b6o5p3?file=/src/index.js

    Your actual issue, however, is that you're overwriting your state.users whenever you search for anything in the input. Because by doing state.users = state.users.filter(... or even the way I suggested above: return {...state, users: state.users.filter..., you will lose your original users data the moment you start typing anything into the search bar.

    Your reducer has no way of going back to the original state of the users list because it doesn't know what the original state was as all it had was the users array, which is being modified.

    Reducer stores should only be used for state-affecting actions. In your case, searchByName/a search bar implementation is something you would actually only want to do inside your component where the search bar + filtered users is being used.

    Solution 1: Get rid of the searchByName user action and do it directly within your component

    const dispatch = useDispatch();
      const { users, isLoading } = useSelector((state) => state.users);
      const [filteredUsers, setFilteredUsers] = useState(users);
      const [searchTerm, setSearchTerm] = useState("");
    
      const changeSearchTerm = (e) => {
        setSearchTerm(e.target.value);
      };
    
      useEffect(() => {
        setFilteredUsers(users.filter((user) => user.name.toLowerCase().includes(action.payload.toLowerCase());
        
      }, [searchTerm]);
    
      // Just for debug/logging purposes to see your filteredUsers
      useEffect(() => {console.log(filteredUsers)}, [filteredUsers]);
    
      return (
        <div>
          <input onChange={changeSearchTerm} type="text" value={searchTerm}></input>
        </div>
      );
    

    Solution 2: If you want to use filteredUsers in other components/across your app, add a separate field to your redux store to track them so that you don't lose the original users field

    const slice = createSlice({
      name: "users",
      initialState: {
        users: [],
        filteredUsers: [],
        isLoading: true,
        search: "",
      },
      reducers: {
        usersSuccess: (state, action) => {
          state.users = action.payload;
          state.isLoading = false;
        searchByName: (state, action) => {
    
          return {
            ...state,
            filteredUsers: [...state.users].filter((user) => user.name.toLowerCase().includes(action.payload.toLowerCase()))
         };
    
        },
      },
    });
    

    Use one or the other, not both solutions!


    Edit: #2 Here's a full working solution Solution #2 (https://codesandbox.io/s/elated-estrela-b6o5p3?file=/src/index.js:211-1748)

    const users = createSlice({
      name: "users",
      initialState: {
        users: [{ name: "aldo" }, { name: "kiv" }],
        filteredUsers: [{ name: "aldo" }, { name: "kiv" }],
        isLoading: true,
        search: ""
      },
      reducers: {
        usersSuccess: (state, action) => {
          state.users = action.payload;
          state.isLoading = false;
          return {
            users: action.payload,
            filteredUsers: [...action.payload],
            isLoading: false
          };
        },
        searchByName: (state, action) => {
          const filteredUsers = state.users.filter((user) =>
            user.name.toLowerCase().includes(action.payload.toLowerCase())
          );
          return {
            ...state,
            filteredUsers:
              action.payload.length > 0 ? filteredUsers : [...state.users]
          };
        }
      }
    });
    
    const userActions = users.actions;
    
    // store
    var store = createStore(users.reducer);
    
    // component
    function App() {
      const dispatch = useDispatch();
      const users = useSelector((state) => state.users);
      const filteredUsers = useSelector((state) => state.filteredUsers);
      const [searchTerm, setSearchTerm] = useState("");
    
      console.log(users);
    
      const changeSearchTerm = (e) => {
        setSearchTerm(e.target.value);
      };
    
      useEffect(() => {
        dispatch(userActions.searchByName(searchTerm));
      }, [searchTerm, dispatch]);
    
      return (
        <div>
          <input onChange={changeSearchTerm} type="text" value={searchTerm} />
          <div>
            {filteredUsers.map((user) => (
              <div>{user.name}</div>
            ))}
          </div>
        </div>
      );
    }
    
    // --root store config
    const store = createStore(users.reducer);
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById("root")
    );