I have a parent react component containing 3 children:
<ChildComponent category="foo" />
<ChildComponent category="bar" />
<ChildComponent category="baz" />
The child component calls an api depending on the prop category value:
http://example.com/listings.json?category=foo
In my action the data is returned as expected. However, when the child component renders the data. The last child baz is overwriting its value in foo and bar as well.
A solution to this problem seems to be given here. But I would like this to be dynamic and only depend on the category prop. Is this not possible to do in Redux?
My child component looks like this:
class TweetColumn extends Component {
componentDidMount() {
this.props.fetchTweets(this.props.column)
}
render() {
const { tweets, column } = this.props
if (tweets.length === 0) { return null }
const tweetItems = tweets[column].map(tweet => (
<div key={ tweet.id }>
{ tweetItems.name }
</div>
)
return (
<div className="box-content">
{ tweetItems }
</div>
)
}
}
TweetColumn.propTypes = {
fetchTweets: PropTypes.func.isRequired,
tweets: PropTypes.array.isRequired
}
const mapStateToProps = state => ({
tweets: state.tweets.items
})
export default connect(mapStateToProps, { fetchTweets })( TweetColumn )
reducers:
export default function tweetReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TWEETS_SUCCESS:
return {
...state,
[action.data[0].user.screen_name]: action.data
}
default:
return state;
}
}
export default combineReducers({
tweets: tweetReducer,
})
action:
export const fetchTweets = (column) => dispatch => {
dispatch({ type: FETCH_TWEETS_START })
const url = `${ TWITTER_API }/statuses/user_timeline.json?count=30&screen_name=${ column }`
return axios.get(url)
.then(response => dispatch({
type: FETCH_TWEETS_SUCCESS,
data: response.data
}))
.then(response => console.log(response.data))
.catch(e => dispatch({type: FETCH_TWEETS_FAIL}))
}
You are making an api call every time TweetColumn is mounted. If you have multiple TweetColumn components and each one makes an api call, then whichever one's response is last to arrive is going to set the value of state.tweets.items. That's because you are dispatching the same action FETCH_TWEETS_SUCCESS every time (the last one overrides the previous one). To solve that issue, assuming the response has a category (foo, bar, baz), I would write the reducer in the following way:
export default function tweetReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TWEETS_SUCCESS:
return {
...state,
[action.data.category]: action.data
}
default:
return state;
}
}
You can then do the following in your TweetColumn component:
class TweetColumn extends Component {
componentDidMount() {
this.props.fetchTweets(this.props.column)
}
render() {
const { column } = this.props;
const tweetItems = this.props.tweets[column].map(tweet => (
<div key={ tweet.id }>
{ tweet.name }
</div>
)
return (
<div className="box-content">
{ tweetItems }
</div>
)
}
}
const mapStateToProps = state => ({
tweets: state.tweets
})
const mapDispatchToProps = dispatch => ({
fetchTweets: column => dispatch(fetchTweets(column))
})
export default connect(
mapStateToProps,
mapDispatchToProps,
)( TweetColumn )
You will have to do some validation to make sure tweets[column] exists, but you get the idea.
Related
I'm learning Redux state management and got stuck with an issue. The mapStateToProps within a component is not triggered when the state changes. Gone through a lot of blogs, couldn't able to figure out the problem.
The store seems to update properly, as the "subscribe" method on store prints new changes. Attached screenshot as well.
Actions.js
export const GET_ITEMS_SUCCESS = "GET_ITEMS_SUCCESS";
export const GET_ITEMS_FAILURE = "GET_ITEMS_FAILURE";
export const getItemsSuccess = (items) => ({
type: GET_ITEMS_SUCCESS, payload: items
});
export const getItemsFailure = (error) => ({
type: GET_ITEMS_FAILURE, error: error
});
export function getItems(dispatch) {
return dispatch => {
fetch(myList)
.then(res => res.json())
.then(res => {
if(res.error) {
throw(res.error);
}
store.dispatch(getItemsSuccess(res));
return res;
})
.catch(error => {
store.dispatch(getItemsFailure(error));
})
}
}
Reducer
let initialState = {items: [], error: null}
function GetItemsReducer (state = initialState, action) {
switch (action.type) {
case GET_ITEMS_SUCCESS:
return Object.assign({}, state, {pending: false, items: action.payload});
case GET_ITEMS_FAILURE:
return Object.assign({}, state, {pending: false, error: action.error});
default:
return state;
}
}
export default const rootReducer = combineReducers({
GetItemsReducer: GetItemsReducer
});
Store
const mystore = createStore(rootReducer, applyMiddleware(thunk, promise));
mystore.subscribe(() => console.log("State Changed;", mystore.getState()));
Component
class Home extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.fetchItems();
}
render() {
return (
<div>{this.props.items.length}</div>
)
}
}
const mapStateToProps = (state) => {
console.log('mapStateToProps ----------> ', state);
return {
items: state.GetItemsReducer.items,
error: state.GetItemsReducer.error
}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchItems: bindActionCreators(getItems, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
Main
class App extends React.Component {
render() {
return (
<Provider store={mystore}>
<Home />
</Provider>
)
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
Thanks in advance.
I have two redux state variables, one that hold an array of user information and one that holds a true/false value for a side drawer open/close feature. The true/false value triggers a className change which triggers CSS to open/close the drawer. The array of user information is fetched from a firebase cloud firestore database collection.
For some reason after the user array is fetched and saved to the redux state and a user opens the side drawer the redux action sent is only for the side drawer, but the side drawer and users information is changed.
The side drawer opens like normal, but the user array is set to null.
Redux Events:
Initial State: https://imgur.com/a/IgvXMLe
After side drawer is opened: https://imgur.com/a/wVRg6Az
Side Drawer Event Difference: https://imgur.com/a/u1hrcvT
Side Drawer Component
class SideDrawer extends Component {
render() {
let drawerClasses = ['side-drawer'];
if (this.props.toggled) {
drawerClasses = ['side-drawer', 'open'];
}
return (
<div className={drawerClasses.join(' ')} >
<div className="side-drawer-container" >
<div className="router-login-button" onClick={this.props.toggleSideDrawer} >
<OktaAuthButton />
</div>
<div className="side-drawer-link" onClick={this.props.toggleSideDrawer} >
<Link to="/" >Map</Link>
</div>
<div className="side-drawer-link" onClick={this.props.toggleSideDrawer} >
<Users />
</div>
</div>
</div>
)
}
}
const mapStateToProps = ({ sideDrawer }) => ({
toggled: sideDrawer.toggled,
});
const mapDispatchToProps = (dispatch) => {
return {
toggleSideDrawer: () => dispatch({ type: TOGGLE_SIDEDRAWER, payload: true })
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SideDrawer);
Side Drawer Reducer
import { TOGGLE_SIDEDRAWER } from './actions';
const initialState = {
toggled: false
};
export default function sideDrawerReducer(state = initialState, action) {
switch (action.type) {
case TOGGLE_SIDEDRAWER:
return Object.assign({}, state, {
toggled: action.payload
});
default:
return state;
}
}
Users Component
class Users extends Component {
/* commented code not needed to be shown */
componentDidMount() {
initializeFirebaseApp();
// Get user list from firestore 'users' collection
this.loadUsers();
}
async loadUsers() {
getAllUsers().then((users) => {
this.props.setUsers(users);
});
}
render() {
if(this.props.users != null) {
var users = this.props.users.map((el, i) => (
<li key={el.id} className='user' onClick={this.props.toggleSideDrawer}><Link to={"/user/" + el.id}>{el.firstname}</Link></li>
));
console.log(users);
}
console.log(this.props.users);
return (
<div className="user-container">
{users}
</div>
)
}
}
const mapStateToProps = ({ users }) => ({
users: users.friends
});
const mapDispatchToProps = (dispatch) => {
return {
setUsers: (users) => dispatch({type: SET_FRIENDS, payload: users}),
toggleSideDrawer: () => dispatch({ type: TOGGLE_SIDEDRAWER, payload: false })
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Users);
Users Reducer
import { SET_FRIENDS } from './actions';
const initialState = {
friends: null,
groups: null
};
export default function userReducer(state = initialState, action) {
switch(action.type) {
case SET_FRIENDS:
return Object.assign({}, state, {
friends: action.payload
});
default:
return initialState;
}
}
I expect the side drawer to open and render the list of users in the drawer under the "Login" and "Map" Links
The default case for userReducer is returning initialState instead of state so every action through the redux store that is not SET_FRIENDS (e.g. TOGGLE_SIDEDRAWER) will reset the userReducer to initialState where users is null. Return state instead and you should be good to go.
export default function userReducer(state = initialState, action) {
switch(action.type) {
case SET_FRIENDS:
return Object.assign({}, state, {
friends: action.payload
});
// Change to `return state;`
default:
return initialState;
}
}
I have this class that shows a list of cars:
render() {
return (
<div>
{this.props.cars.map((car) => <Car key={car.Id} car={car} />)}
</div>
);
}
I'm making a http request to get the cars in my api:
componentDidMount() {
axios.get(`http://localhost:8000/api/cars`)
.then(res => {
const cars= res.data.records;
this.props.dispatch({
type:'GET_CARS',
cars});
})
}
}
export default connect(mapStateToProps)(ListCars);
How i can make a reducer that add to props.cars the return of axios get? my actually reducer is don't working:
const CarReducer= (state = [], action) => {
switch(action.type) {
case 'ADD_CAR':
return state.concat([action.data]);
case 'GET_CARS':
return state.map(car =>
car
)
default:
return state;
}
}
export default carReducer
You dispatched this plain object to Redux:
this.props.dispatch({ type:'GET_CARS', cars })
So, you will be able to receive your data from action.cars in your reducer and add it to your Redux store:
const CarReducer = (state = [], action) => {
switch(action.type) {
case 'ADD_CAR':
return state.concat([action.data]);
case 'GET_CARS':
// Receive data from action.cars
// I am assuming your data is in an array
return [...state, ...action.cars];
default:
return state;
}
}
Then, in your component, you should be able to receive the data from the API by using connect() from react-redux:
import { connect } from 'react-redux';
class App extends React.Component {
render() {
return (
<div>
{this.props.cars.map((car) =>
<Car key={car.Id} car={car} />
)}
</div>
);
}
}
const mapStateToProps = state => {
return { cars: state.cars }
}
export default connect(mapStateToProps)(App)
I have a form in react where I'm asking for the last 8 of the VIN of a car. Once I get that info, I want to use it to get all the locations of the car. How do I do this? I want to call the action and then display the results.
Added reducer and actions...
Here is what I have so far...
class TaglocaByVIN extends Component {
constructor(props){
super(props);
this.state={
searchvin: ''
}
this.handleFormSubmit=this.handleFormSubmit.bind(this);
this.changeText=this.changeText.bind(this);
}
handleFormSubmit(e){
e.preventDefault();
let searchvin=this.state.searchvin;
//I want to maybe call the action and then display results
}
changeText(e){
this.setState({
searchvin: e.target.value
})
}
render(){
return (
<div>
<form onSubmit={this.handleFormSubmit}>
<label>Please provide the last 8 characters of VIN: </label>
<input type="text" name="searchvin" value={this.state.searchvin}
onChange={this.changeText}/>
<button type="submit">Submit</button>
</form>
</div>
);
}
}
export default TaglocaByVIN;
Here are my actions:
export function taglocationsHaveError(bool) {
return {
type: 'TAGLOCATIONS_HAVE_ERROR',
hasError: bool
};
}
export function taglocationsAreLoading(bool) {
return {
type: 'TAGLOCATIONS_ARE_LOADING',
isLoading: bool
};
}
export function taglocationsFetchDataSuccess(items) {
return {
type: 'TAGLOCATIONS_FETCH_DATA_SUCCESS',
items
};
}
export function tagformsubmit(data){
return(dispatch) =>{
axios.get(`http://***`+data)
.then((response) => {
dispatch(taglocationsFetchDataSuccess);
})
};
}
reducer:
export function tagformsubmit(state=[], action){
switch (action.type){
case 'GET_TAG_FORM_TYPE':
return action.taglocations;
default:
return state;
}
}
This is an easy fix but it will take a few steps:
Set up an action
Set up your reducer
Fetch and Render data in component
Creating the Action
The first thing, you need to set up an action for getting data based on a VIN. It looks like you have that with your tagformsubmit function. I would make a few adjustments here.
You should include a catch so you know if something went wrong, change your function param to include dispatch, add a type and a payload to your dispatch, and fix the string literal in your api address. Seems like a lot but its a quick fix.
Update your current code from this:
export function tagformsubmit(data){
return(dispatch) =>{
axios.get(`http://***`+data)
.then((response) => {
dispatch(taglocationsFetchDataSuccess);
})
};
}
to this here:
//Get Tag Form Submit
export const getTagFormSubmit = vin => dispatch => {
dispatch(loadingFunctionPossibly()); //optional
axios
.get(`/api/path/for/route/${vin}`) //notice the ${} here, that is how you use variable here
.then(res =>
dispatch({
type: GET_TAG_FORM_TYPE,
payload: res.data
})
)
.catch(err =>
dispatch({
type: GET_TAG_FORM_TYPE,
payload: null
})
);
};
Creating the Reducer
Not sure if you have already created your reducer. If you have you can ignore this. Creating your reducer is also pretty simple. First you want to define your initial state then export your function.
Example
const initialState = {
tags: [],
tag: {},
loading: false
};
export default (state=initialState, action) => {
if(action.type === GET_TAG_FORM_TYPE){
return {
...state,
tags: action.payload,
loading: false //optional
}
}
if(action.type === GET_TAG_TYPE){
return {
...state,
tag: action.payload,
}
}
}
Now that you have your action and reducer let's set up your component.
Component
I'm going to assume you know all of the necessary imports. At the bottom of your component, you want to define your proptypes.
TaglocaByVIN.propTypes = {
getTagFormSubmit: PropTypes.func.isRequired,
tag: PropTypes.object.isRequired
};
mapStateToProps:
const mapStateToProps = state => ({
tag: state.tag
});
connect to component:
export default connect(mapStateToProps, { getTagFormSubmit })(TaglocaByVIN);
Update your submit to both pass data to your function and get the data that is returned.
handleFormSubmit = (e) => {
e.preventDefault();
const { searchvin } = this.state;
this.props.getTagFormSubmit(searchvin);
const { tags } = this.props;
tags.map(tag => {
//do something with that tag
}
Putting that all together your component should look like this (not including imports):
class TaglocaByVIN extends Component {
state = {
searchvin: ""
};
handleFormSubmit = e => {
e.preventDefault();
const { searchvin } = this.state;
this.props.getTagFormSubmit(searchvin);
const { tags } = this.props.tag;
if(tags === null){
//do nothing
} else{
tags.map(tag => {
//do something with that tag
});
};
}
changeText = e => {
this.setState({
searchvin: e.target.value
});
};
render() {
return (
<div>
<form onSubmit={this.handleFormSubmit}>
<label>Please provide the last 8 characters of VIN: </label>
<input
type="text"
name="searchvin"
value={this.state.searchvin}
onChange={this.changeText}
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
}
TaglocaByVIN.propTypes = {
getTagFormSubmit: PropTypes.func.isRequired,
tag: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
tag: state.tag
});
export default connect(
mapStateToProps,
{ getTagFormSubmit }
)(TaglocaByVIN);
That should be it. Hope this helps.
The action dispatch is not working, The function works and I get the console.log but the store isn't changing. Any ideas?
import React from 'react';
import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import RemoveTodo from './RemoveTodo';
import { remove } from '../actions/Todo';
import { store } from '../app';
class TodosSummary extends React.Component {
constructor(props) {
super(props);
}
onDelete = ({id}) => {
store.dispatch(remove({id}))
console.log(store.getState());
};
render () {
return (
<ul>
{this.props.target.map(({todo, significance, id}) => {
return (
<li
key={id}>{todo} - impact is {significance}
<button onClick={this.onDelete}>Remove</button>
</li>
);
})}
</ul>
</div>
);
}
};
const mapStateToProps = (state) => {
return {
target: state.target
}; };
export default connect(mapStateToProps)(TodosSummary)
This is the action, taking the todo id
export const remove = ({id}) => ({
type: 'REMOVE_TODO',
id
});
And that's the reducer, filtering the state and bringing back the filtered array
const todosReducer = (state = todosReducerDefaultState, action) => {
switch(action.type) {
case 'ADD_TODO':
return [
...state,
action.target
];
case 'REMOVE_TODO':
return (
state.filter(({ id }) => id !== action.id)
);
I see you want to access target property inside state. So, the reducer should be like this:
case 'ADD_TODO':
return {
...state,
target: [...state.target, action.target]
};
case 'REMOVE_TODO':
return {
...state,
target: state.target.filter(({ id }) => id !== action.id)
};
See if this works.
Return updated state like this.
case 'REMOVE_TODO':
return [...state.filter(({ id }) => id !== action.id)];
It's not suggested to change the state directly, please change the addToDo to the following.
Object.assign({}, state, {
todoList: state.target
});
Can you please provide the code snippet for the default state as per your code 'todosReducerDefaultState'?