I am practicing the react-redux.
I wonder what is the difference between these two codes.
I think these have same meaning, but the first code is not working well.
The first one cannot deliver the props of the state.
I removed the "import" part in this post
const TodosContainer = ({
input,
todos,
changeInput,
insert,
toggle,
remove
}) => {
return (<Todos
input={input}
todos={todos}
onChangeInput={changeInput}
onInsert={insert}
onToggle={toggle}
onRemove={remove} />);
};
// from this point, I changed this code into below one
const mapStateToProps = state => ({ // in this part, I think it cannot send props
input: state.todos.input,
todos: state.todos.todos
});
const mapDispatchToProps = dispatch => ({
changeInput: () => {
dispatch(changeInput());
},
insert: () => {
dispatch(insert());
},
toggle: () => {
dispatch(toggle());
},
remove: () => {
dispatch(remove());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps)(TodosContainer);
export default connect(
({ todos }) => ({
input: todos.input,
todos: todos.todos,
}),
{
changeInput,
insert,
toggle,
remove,
}
)(TodosContainer);
Related
I have a situation here where there are two components. the first component have an input field and the other component is displaying icon and a title name. i used Redux toolkit where when the input field is written anything the icon is changed to another icon. the problem is the icon doesn't change i don't why so, i want to know how to define the icon in the component as shown in my code below.
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
icon: "unTick",
value: "",
};
const tickSlice = createSlice({
name: "tickSign",
initialState,
reducers: {
setValue: (state, action) => {
state.value = action.payload;
},
changeIcon: (state) => {
if (state.icon === "unTick") {
state.icon = "tick";
} else {
state.icon = "unTick";
}
},
},
});
export const { changeIcon, setValue } = tickSlice.actions;
export const selectValue = (state) => state.value;
export const selectIcon = (state) => state.icon;
export default tickSlice.reducer;
the code of the first component of input
const value = useSelector(selectValue);
const dispatch = useDispatch();
const handleChange = useCallback(
(e) => {
dispatch(setValue(e.target.value));
if (e.target.value.trim().length !== 0) {
dispatch(changeIcon("tick"));
} else {
dispatch(changeIcon("unTick"));
}
},
[dispatch]
);
<input
id="boxInput"
type="text"
value={value}
placeholder="Enter Apprenticeship Title"
onChange={handleChange}
/>
the code for the icon change component
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { selectIcon } from "../features/TickSlice";
import { TickCircle, UnTickCircle } from "./IconSvg";
const DescNavTitles = ({ title }) => {
const icon = useSelector(selectIcon);
const dispatch = useDispatch();
return (
<div className="navTitle">
<svg>{icon === "tick" ? <TickCircle /> : <UnTickCircle />}</svg>
<p className="navTitle__name">{title}</p>
</div>
);
};
export default DescNavTitles;
So, i don't know where exactly should i define the icon to be displayed and changed dynamically
components on the page
Redux tool when written
Redux tool when not written
You are not selecting the icon state correctly. Based on the screen captures showing the Redux state from the devtools your root state has two properties, appren and tick.
The Redux state:
{
appren: {
boxItems: [....],
title: "",
position: "",
...
},
tick: {
icon: "....",
value: "...."
},
}
I'm assuming it's this state.tick that is the tickSign slice you are working with. Recall that the useSelector hook callback is passed the entire Redux state object.
const icon = useSelector(state => state.tick.icon);
The selectValue and selectIcon selector functions need to access the correct path to access the expected state properties.
export const selectValue = (state) => state.tick.value;
export const selectIcon = (state) => state.tick.icon;
Additionally, the changeIcon action is only toggling the state value when dispatched, but it seems you want to conditionally set the value from an input element's onChange handler. I suggest the following refactor:
const initialState = {
isTicked: false,
value: "",
};
const tickSlice = createSlice({
name: "tickSign",
initialState,
reducers: {
setValue: (state, action) => {
state.value = action.payload;
},
changeIsTicked: (state, action) => {
state.isTicked = action.payload;
},
},
});
export const { changeIcon, setValue } = tickSlice.actions;
export const selectValue = (state) => state.tick.value;
export const selectIsTicked = (state) => state.tick.isTicked;
const value = useSelector(selectValue);
const dispatch = useDispatch();
const handleChange = useCallback((e) => {
dispatch(setValue(e.target.value));
dispatch(changeIsTicked(e.target.value.trim().length !== 0));
}, [dispatch]);
<input
id="boxInput"
type="text"
value={value}
placeholder="Enter Apprenticeship Title"
onChange={handleChange}
/>
const DescNavTitles = ({ title }) => {
const isTicked = useSelector(selectIsTicked);
return (
<div className="navTitle">
<svg>{isTicked ? <TickCircle /> : <UnTickCircle />}</svg>
<p className="navTitle__name">{title}</p>
</div>
);
};
The isTicked state is actually unnecessary though as it can be completely derived from the state.tick.value state value. You can create a selector function that computes this. Example:
const initialState = {
value: "",
};
const tickSlice = createSlice({
name: "tickSign",
initialState,
reducers: {
setValue: (state, action) => {
state.value = action.payload;
},
},
});
export const { changeIcon, setValue } = tickSlice.actions;
export const selectValue = (state) => state.tick.value;
export const selectIsTicked = (state) => !!state.tick.value.length;
const DescNavTitles = ({ title }) => {
const isTicked = useSelector(selectIsTicked);
return (
<div className="navTitle">
<svg>{selectIsTicked ? <TickCircle /> : <UnTickCircle />}</svg>
<p className="navTitle__name">{title}</p>
</div>
);
};
In the component's render method, you are using the useSelector hook to get the current icon value from the state and conditionally render either TickCircle or UnTickCircle component based on the icon value.
const icon = useSelector(selectIcon);
return (
<div className="navTitle">
<svg>{icon === "tick" ? <TickCircle /> : <UnTickCircle />}</svg>
<p className="navTitle__name">{title}</p>
</div>
);
Try this.
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} />
))}
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")
);
I have a simple react app, which I am using to display a list of users. I have a search box, which filters the list of users and returns the searched user,
However, When I search for users using the search box, I get a TypeError: Cannot read property 'filter' of undefined
The page initially displays fine, it just when I search for the users that the error is thrown. I know the users are being generated correctly because I console logged filteredUsers The users are all outputted correctly.
Here is my App.js
import { useState } from 'react';
import React from 'react';
import CardList from './components/CardList';
import SearchBox from './components/SearchBox';
import { users } from './users';
import 'tachyons';
const App = () => {
const [state, setState] = useState({
users,
searchField: '',
});
const filteredUsers = state.users.filter((user) => {
return user.name.toLowerCase().includes(state.searchField.toLowerCase());
});
const onSearchChange = (e) => {
setState({ searchField: e.target.value });
};
return (
<div className='App tc'>
<h1>RoboFriends</h1>
<SearchBox searchChange={onSearchChange} />
<CardList robots={filteredUsers} />
</div>
);
};
export default App;
and here is my SearchBox.js
import React from 'react';
const SearchBox = ({ searchField, searchChange }) => {
return (
<div className='pa2'>
<input
className='pa3 ba b--green bg-lightest-blue'
type='search'
placeholder='Search Users'
onChange={searchChange}
/>
</div>
);
};
export default SearchBox;
Because you are using state.users here, filter expect it to be an array.
const filteredUsers = state.users.filter((user) => {
return user.name.toLowerCase().includes(state.searchField.toLowerCase());
});
so you need to set the initial state as an array
const [state, setState] = useState({
users: [], //or a initial value of array of users.
searchField: '',
});
You also need to retain the users when you update the state.
const onSearchChange = (e) => {
setState( prev => ({ ...prev, searchField: e.target.value }));
};
//OR
const onSearchChange = (e) => {
setState({ ...state, searchField: e.target.value });
};
However, this line is not good because it's being called on every render, you should probably "upgrade" it with useMemo like this.
const filteredUsers = useMemo(() => state.users.filter((user) => {
return user.name.toLowerCase().includes(state.searchField.toLowerCase());
}), [state])
Change
const onSearchChange = (e) => {
setState({ searchField: e.target.value });
};
to
const onSearchChange = (e) => {
setState({ searchField: e.target.name.value });
};
And make users into an array
I want to implement an action which gets item by id, so I've created fetchItemAction(), as follows:
export const fetchItemAction = () => (dispatch) => {
dispatch({
type: FETCH_ITEM_REQUEST,
});
return axios.get(`${url}/notes/5d4724cd62087b0e141f75a4`)
.then(({ data }) => {
console.log(data);
dispatch({
type: FETCH_ITEM_SUCCESS,
data,
});
})
.catch((error) => {
dispatch({
type: FETCH_ITEM_FAILURE,
});
});
};
Then, I try to set item field in State in my reducer:
const initialState = {
isAuthenticated: false,
user: {},
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ITEM_REQUEST:
return {
...state,
isLoading: true,
};
case FETCH_ITEM_SUCCESS:
return {
...state,
item: action.data,
isLoading: false,
};
}
};
Then, I try to get those data in Details component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchItemAction } from 'actions/actions';
class Details extends Component {
componentDidMount() {
const { fetchItem } = this.props;
fetchItem();
}
render() {
const { item, isLoading } = this.props;
return (
<>
{console.log(item)}
{/* <p>{item.title}</p> */}
</>
);
}
}
const mapStateToProps = ({ item, isLoading }) => ({ item, isLoading });
const mapDispatchToProps = dispatch => ({
fetchItem: () => dispatch(fetchItemAction()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Details);
As a result, I'm getting following data in console:
Apart from two undefinded the result looks good because there is correct response from my backend.
But, when I try to uncomment <p>item.title</p> line in Details.js, the app crash:
TypeError: Cannot read property 'title' of undefined
I also implemented correctly fetchItemsAction(), addItemAction() and deleteItemAction() which are very similar but I have no idea what is wrong in fetchItemAction().
This is an asynchronous issue. componentDidMount is called when the component is mounted. Then, you're calling fetch. So on your first render, item is undefined. Once the data is returned, the render is triggered again with item data.
So, just check if item is defined:
render() {
const { item, isLoading } = this.props;
return (
<>
{console.log(item)}
{item && <p>{item.title}</p>}
</>
);
}
i have some issues dealing with a simple case in my redux-react app: i want to reset an input text after an asynchronous operation ignited by a button.
Let’s say we have an input text in which you put a text and this is passed through a onClick event to a dispatch action.
This action contacts a server and after the server response i want to reset the input field.
I’ve implemented a number of solutions (i’m using redux thunk) to this problem but i’m not sure if they are hacky ways to solve it, let me show you:
1) Presentational component (the input field) implements a reset method that is passed as a value to the onClick method.
export default React.createClass({
reset: function () {
this.setState({searchText: ''})
},
getInitialState: function () {
return {
searchText: ''
}
},
render: function () {
return (
<div>
<TextField
value={this.state.searchText}
onChange={e => this.setState({ searchText: e.target.value })}
/>
<RaisedButton
onClick={this.props.startSearch.bind(null,
this.state.searchText,
this.reset)} // ===> HERE THE RESET FUNCTION IS PASSED
/>
</div>
)
}
})
The container dispatches the action and then calls the reset method.
const mapDispatchToProps = (dispatch) => {
return {
startSearch: (searchText, reset) => {
dispatch(actions.startSearch(searchText))
.then(() => reset())
}
}
}
2) Using ref (https://facebook.github.io/react/docs/refs-and-the-dom.html)
The container gets a reference to its child and calls reset through it
const SearchUserContainer = React.createClass({
startSearch: (searchText) => {
dispatch(actions.startSearch(searchText))
.then(() => this.child.reset())
},
render: function () {
return (
<SearchUser {...this.props} ref={(child) => { this.child = child; }}/>
)
}
})
3) The Redux Way.
searchText is managed by the store thus the action dispatched triggers a resolver that reset the searchText value, the container updates its child and we are done, well… almost:
the presentational component is a controlled component (https://facebook.github.io/react/docs/forms.html#controlled-components) that means it manages the input text as an internal state, i think we have to find a way to make the two ‘state managers’ coexist.
I wrote this code to manage the internal state and the state coming from redux, in few words the presentational gets the initial value from redux, then updates it in the onChange event and it’s ready to receive updates from redux thanks to componentWillReceiveProps.
export default React.createClass({
getInitialState: function () {
return {
searchText: this.props.searchText ==> REDUX
}
},
componentWillReceiveProps: function (nextProps) {
this.setState({
searchText: nextProps.searchText ==> REDUX
})
},
render: function () {
return (
<div>
<TextField
value={this.state.searchText}
onChange={e => this.setState({ searchText: e.target.value })}
/>
<RaisedButton
onClick={this.props.startSearch.bind(null, this.state.searchText)}
/>
</div>
)
}
})
4) Redux-Form
To complete the picture i link the redux-form options to do that
http://redux-form.com/6.5.0/docs/faq/HowToClear.md/
What do you think about those ideas?
Thanks.
Go the Redux way, except go all the way: remove the internal state from your component completely and let Redux handle it (might as well make your component a pure-functional component too):
Component:
import { connect } from 'redux';
import { actions } from 'actionCreators';
const ControlledInputComponent = (props) => {
return (
<div>
<TextField
value={this.props.searchText}
onChange={e => this.props.setSearchText(e.target.value)}
/>
<RaisedButton
onClick={this.props.startSearch}
/>
</div>
);
};
const mapStateToProps = (state) => {
return { searchText: state.searchText };
};
const mapDispatchToProps = (dispatch) => {
return {
setSearchText: (txt) => { dispatch(actions.setSearchText(txt)); },
startSearch: () => { dispatch(actions.search()); }
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ControlledInputComponent);
Action creator:
export const actions = {
setSearchText: (txt) => ({ type: 'setText', data: txt }),
//here's where the thunk comes in
//make sure you have redux-thunk and add it as a middleware when setting up the store, etc.
search: () => {
return (dispatch) => {
//use fetch or whatever to run your search (this is a simplified example)
fetch(/* your url here */).then(() => {
//presumably a success condition
//handle the search results appropriately...
//dispatch again to reset the search text
dispatch(actions.setSearchText(null);
});
};
}
};
Reducer:
const reducer = (state = { searchText: null }, action) => {
if (!action || !action.type) return state;
switch (action.type) {
//you should really define 'setText' as a constant somewhere
//so you can import it and not have to worry about typos later
case 'setText':
return Object.assign({}, state, { searchText: action.data });
default:
return state;
}
};
export default reducer;
Hopefully that helps. Good luck!