Reputation: 63
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} />
))}
Upvotes: 5
Views: 11296
Reputation: 1866
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")
);
Upvotes: 9