I'm fairly new to redux & thunk, and have been following tutorials to try and understand, and am managing to work it into my app ok. One thing i'm not understanding, is how i can get several state objects on the root level into one nested object. For example, right now my state looks like:
{
timeline: [Array] // My timeline data in an array of objects
timelineHasErrored: false,
timelineIsLoading: false
}
But what I really want is:
{
timeline : {
data: [Array] // My timeline data in an array of objects
hasErrored: false,
isLoading: false
}
}
and i'm really not quite sure how to nest these, or what the proper way to do that is. Below is my redux code, it's pretty simple so i'll post it all.
Reducers index
import { combineReducers } from 'redux'
import { timeline, timelineHasErrored, timelineIsLoading } from './timeline'
export default combineReducers({
timeline, timelineHasErrored, timelineIsLoading
});
Timeline Reducers
import { TIMELINE_HAS_ERRORED, TIMELINE_IS_LOADING, TIMELINE_FETCH_DATA_SUCCESS } from '../constants/action-types.js'
export function timelineHasErrored(state = false, action) {
switch (action.type) {
case TIMELINE_HAS_ERRORED:
return action.hasErrored;
default:
return state;
}
}
export function timelineIsLoading(state = false, action) {
switch (action.type) {
case TIMELINE_IS_LOADING:
return action.isLoading;
default:
return state;
}
}
export function timeline(state = [], action) {
switch (action.type) {
case TIMELINE_FETCH_DATA_SUCCESS:
return action.timeline;
default:
return state;
}
}
Actions
import { TIMELINE_HAS_ERRORED, TIMELINE_IS_LOADING, TIMELINE_FETCH_DATA_SUCCESS } from '../constants/action-types.js'
import api from '../services/api'
export function timelineHasErrored(bool) {
return {
type : TIMELINE_HAS_ERRORED,
hasErrored : bool
}
}
export function timelineIsLoading(bool) {
return {
type : TIMELINE_IS_LOADING,
isLoading : bool
}
}
export function timelineFetchDataSuccess(timeline) {
return {
type : TIMELINE_FETCH_DATA_SUCCESS,
timeline
}
}
export function timelineFetchData() {
return dispatch => {
dispatch( timelineIsLoading(true) )
api.getTracks().then(
res => {
dispatch( timelineIsLoading(false) )
dispatch( timelineFetchDataSuccess(res.body) )
},
err => {
dispatch( timelineIsLoading(false) )
dispatch( timelineHasErrored(true) )
}
)
}
}
And then in my react component I format the object like how i want it... but i think it would be better to have it nested in the actual state so i'm not creating extra work for myself if things change
// Redux State
const mapStateToProps = (state) => {
const obj = {
timeline : {
data : state.timeline,
hasErrored: state.tracksHasErrored,
isLoading: state.tracksIsLoading
}
}
return obj
}
// Redux Dispatch
const mapDispatchToProps = (dispatch) => {
return {
fetchData: () => dispatch( timelineFetchData() )
}
}
If anybody has any tips or corrections for me bring em on, i'm trying to get a solid grasp on redux, thanks!
Your timeline reducer is pretty small, so you could have it as a single reducer as follows:
const initialState = {
data: [],
hasErrored: false,
isLoading: false
};
export function timeline(state = initialState, action) {
switch (action.type) {
case TIMELINE_HAS_ERRORED:
return {
...state,
hasErrored: action.hasErrored
};
case TIMELINE_IS_LOADING:
return {
...state,
isLoading: action.isLoading
};
case TIMELINE_FETCH_DATA_SUCCESS:
return {
...state,
data: action.timeline
};
default:
return state;
}
}
Then you wouldn't need to call combineReducers(), unless you had other reducers.
Related
I build an app in React with Redux and I try to send to my state an object and I try to save it in 'thisUser' but I don't know how to write that 'return' because mine doesn't work.
My Redux state:
const initialState = {
thisUser: {}
}
export function usersReducer(state = initialState, action) {
switch (action.type) {
case 'users/addUser':
return { ...state, thisUser: { ...state.thisUser, ...action.payload} } //the problem
default:
return state
}
}
Dispatch method:
dispatch({ type: "users/addUser", payload: new_user });
Can you tell me how to write that return, please?
If you want to append new user then why are you using object type. You should use Array Type thisUser.
const initialState = {
thisUser: []
}
export function usersReducer(state = initialState, action) {
switch (action.type) {
case 'users/addUser':
return { ...state, thisUser: [ ...state.thisUser,action.payload ] }
default:
return state
}
}
Or
If you want to save only single user object then change only that line in your code:
return { ...state, thisUser: action.payload }
It's better to use an array type for if you have a list of users .
If you have a case when you need to use an object just change the brackets [ ] on my code to curly braces { } .
const initialState = {
thisUser: [],
}
export function usersReducer(state = initialState, action) {
switch (action.type) {
case 'users/addUser':
return { ...state, thisUser: [ ...state.thisUser, ...action.payload]}
default:
return state
}
}
I have the following Reducer:
const initialState = {}
const dishReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DISHES':
return (action.dishes)
case 'LOAD_DISHES_ERROR':
console.log("load dishes error")
return state
case 'LOAD_DISHES_SUCCESS':
console.log("load dishes success")
return state
default:
return state;
}
};
export default dishReducer;
And the following action(s):
import {database} from '../../config/fbConfig'
export const startLoadingDishes = (dishes) => {
return (dispatch) =>{
return database.ref('products-dishes').once('value').then((snapshot) => {
let dishes = {}
snapshot.forEach((childSnapshot) => {
let parentkey = childSnapshot.key
let dishArray = [];
childSnapshot.forEach((dish) =>{
dishArray.push(dish.val())
});
dishes[childSnapshot.key] = dishArray;
})
dispatch(loadDishes(dishes))
}).then(() => {
dispatch({ type: 'LOAD_DISHES_SUCCESS' });
}).catch(err => {
dispatch({ type: 'LOAD_DISHES_ERROR' }, err);
});
}
}
export const loadDishes = (dishes) => {
return {
type: 'LOAD_DISHES',
dishes
}
}
The 'startLoadingDishes' action is called inside the componentDidLoad() of a certain Component. However, I want to alter the initial state of my dishReducer so that it includes additional information, as follows:
const initialState = {
value : {},
loaded: false,
loading: false,
error: false
}
So now 'action.dishes' returned by reducer [in 'LOAD_DISHES' case] should be put inside the 'value' part of the state, instead of it being the whole state. Also, the 'loaded' part of the state should be set to true if dishes have already been loaded earlier, and so on. I understand this is fairly simple but as I am new to React+Redux, I don't know how to alter the Action/Reducer codes properly (while keeping state immutability). Any help is appreciated.
I originally asked the question, here is how I solved it (not sure if this is the 'best' way though):
New reducer file:
const initialState = {
value : {},
loaded: false,
loading: false,
error: false
}
const dishReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DISHES':
return {
value: action.dishes,
loading: !state.loading,
loaded: false, //this might need to be set to true
error: false
}
case 'LOAD_DISHES_ERROR':
console.log("load dishes error")
return {
...state, //or: state.value, as the other parts of state are being overwritten below
loaded: false,
loading: false,
error: true
}
case 'LOAD_DISHES_SUCCESS':
console.log("load dishes success")
return {
...state, //better: state.value
loaded: true,
loading: false,
error: false
}
default:
return state;
}
};
export default dishReducer;
No change in actions file.
Now, inside the 'Main' component, I was originally accessing the state as such:
class Main extends Component {
componentDidMount() {
this.props.startLoadingDishes();
}
render() {
return (
//some code
)
}
}
const mapStateToProps = (state) => {
return {
dishes: state.dishes //to access dishes: dishes.value
}
}
export default connect(mapStateToProps, actions)(Main)
The Main component code also stayed the same, with the difference that now I use 'dishes.value' instead of just 'dishes' to access the value of dishes from the state (and dishes.loaded for loaded, and so on). And now the action caller inside componentDidMount is as follows:
componentDidMount() {
if(!this.props.dishes.loaded){
this.props.startLoadingDishes();
console.log("loading dishes from database")
}
}
I have action creator to get data from API and have another action creator for loading status and want change loading status when data completely fetched.
Now, I wrote following codes but not working good, Loading status changes to false before data fetched completely.
My ActionCreator:
export const loadingStatus = (bool) => {
return {
type: Constants.LOADING_STATUS,
isLoading: bool
};
}
const allFlashCards = (action) => {
return{
type: Constants.ALL_CARD,
...action
}
};
export const fetchAllFlashCards = () => {
return (dispatch) => {
dispatch(loadingStatus(true));
return axios.post(API.DISPLAY_ALL_CARDS)
.then((data)=>{
console.warn(data);
dispatch(allFlashCards(data));
dispatch(loadingStatus(false));
}).catch((error)=>{
console.warn(error)
});
}
};
and my Reducer:
const FlashCard = (state = [], action) => {
switch (action.type) {
case Constants.ADD_CARD:
return {...state, data: action.data};
break;
case Constants.ALL_CARD:
return {...state, data: action};
break;
default:
return state;
}
};
export const Loading = (status= false, action) => {
switch (action.type) {
case Constants.LOADING_STATUS:
return action.isLoading;
break;
default:
return status;
}
}
and in my component:
componentDidMount() {
this.props.fetchCards();
}
render() {
return(
<div>
{this.props.loading ?
<Loading/> :
Object.keys(this.props.cards.data).map(this.renderCard)
}
</div>
);
}
const mapStateToProps = (state) => ({
cards: state.main,
loading: state.loading
});
const mapDispatchToProps = (dispatch) => ({
fetchCards: bindActionCreators(fetchAllFlashCards, dispatch)
});
and combineReducer is:
import { combineReducers } from 'redux';
import FlashCard , { Loading } from './FlashCard.js';
import { routerReducer } from "react-router-redux";
export default combineReducers({
main: FlashCard,
loading: Loading,
routing: routerReducer
});
In my page, I have an error in console and it's:
Uncaught TypeError: Cannot read property 'data' of undefined and if put my codes in timeout fixed my bug :/
What should i do?
Your default state is wrong here:
const FlashCard = (state = [], action) => {
switch (action.type) {
case Constants.ADD_CARD:
return {...state, data: action.data};
break;
case Constants.ALL_CARD:
return {...state, data: action};
break;
default:
return state;
}
};
It should be an empty object {} instead of an empty array [], since you're returning objects.
This code
export const fetchAllFlashCards = () => {
return (dispatch) => {
dispatch(loadingStatus(true));
return axios.post(API.DISPLAY_ALL_CARDS)
.then((data)=>{
console.warn(data);
dispatch(allFlashCards(data));
dispatch(loadingStatus(false));
}).catch((error)=>{
console.warn(error)
});
}
};
Looks completely fine. loadingStatus(false) should not be called before setting the flash cards. Your reducers and action creators are synchronous (as they should). So, nothing of note there.
I saw that you're using action.data on the Constants.ADD_CARD action case, but in your code you do not dispatch any actions with that type. Do you do it somewhere else? Maybe that's where the error is?
EDIT:
Another place that you're using .data is in your renderer: this.props.cards.data. What's the value of the state.main?
How are you creating your rootReducer? It should be something like this:
const rootReducer = combineReducers({
main: FlashCard,
loading: Loading,
});
Are you using main there? Or maybe cards?
Finally, I fixed my problem:
In my actionCreator change fetchAllFlashCards method to following:
export const fetchAllFlashCards = () => {
return (dispatch) => {
dispatch(loadingStatus(true));
return axios.post(API.DISPLAY_ALL_CARDS)
.then(({data})=>{
dispatch(allFlashCards(data));
dispatch(loadingStatus(false));
}).catch((error)=>{
console.warn(error)
});
}
};
and in reducer change FlashCard reducer to following:
const FlashCard = (state = [], action) => {
switch (action.type) {
case Constants.ADD_CARD:
return {...state, data: action.data};
break;
case Constants.ALL_CARD:
return {...state, data: action.data};
break;
default:
return state;
}
};
I am new to redux and I am having a hard time understanding how to connect the payload of my API call to my state.
Right now my action.js file looks like this:
import ApiService from '../../services/ApiService';
import { reset } from 'redux-form';
//actions
export const getStock = () => {
return {
type: 'GET_STOCK'
}
}
export const getStockPending = () => {
return {
type: 'GET_STOCK_PENDING'
}
}
export const getStockFulfilled = (stock) => {
return {
type: 'GET_STOCK_FULFILLED',
payload: stock
}
}
export const getStockRejected = () => {
return {
type: 'GET_STOCK_REJECTED'
}
}
// async function calls
export function fetchStocksWithRedux() {
const action_type = "GET_STOCK";
const stock = 'AAPL';
return (dispatch) => {
dispatch({type: `${action_type}_PENDING`});
return ApiService.get(`/search?query=${stock}`)
.then(([response, json]) =>{
if(response.status === 200){
dispatch(getStockFulfilled(json))
}
else{
dispatch(getStockRejected())
}
})
}
}
and my reducer.js file looks like this:
const initialState = {
inProgress: false,
stock: {},
stocks: ['NKE', 'AMZN', 'AAPL'],
error: {}
}
export default (state = initialState, action) => {
switch(action.type) {
case 'GET_STOCK_PENDING':
return {
...state,
inProgress: true,
error: false
}
case 'GET_STOCK_FULFILLED':
return {
...state,
stock: action.payload,
inProgress: false
}
case 'GET_STOCK_REJECTED':
return {
...state,
inProgress: false,
error: action.error
}
default:
return state;
}
}
When I go to call my method fetchStocksWithRedux in my component, the network tab in my dev tools shows a 200 status and the response I'm expecting, but the reducer dispatches the 'GET_STOCK_REJECTED' action, but the error hash is empty. What do you think is going wrong?
Here is my component, for reference:
import React, { Component } from 'react';
import { fetchStocksWithRedux } from '../../redux/modules/Stock/actions';
import { connect } from 'react-redux';
class Dashboard extends Component {
componentDidMount() {
this.props.fetchStocksWithRedux()
}
render() {
return (
<div className="uk-position-center">
</div>
)
}
}
export default connect(
state => ({
stocks: state.stocks,
stock: state.stock
})
, { fetchStocksWithRedux }
)(Dashboard);
Thanks. Any advice or guidance would be greatly appreciated!
I am using redux wth reactjs.
I want to store simple key/value pairs but can't get the reducer syntax right.
In this case each key/value pair will hold a connection to an external system.
Is this the right way to do it? I'm at the beginning with redux so it's a bit of mystery.
export default (state = {}, action) => {
switch(action.type) {
case 'addConnection':
return {
connections: {
...state.connections, {
action.compositeKey: action.connection
}
}
default:
return state
}
}
This worked for me:
export default (state = {}, action) => {
switch(action.type) {
case 'addConnection':
return {
...state,
connections: {
...state.connections,
[action.compositeKey]: action.connection
}
}
default:
return state
}
}
From the docs:
https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns#correct-approach-copying-all-levels-of-nested-data
You just have a couple mistakes with {} instead of [] and forgetting to use Object.assign.
const reducer = (state = {}, action) => {
switch (action.type) {
case 'addConnection':
return Object.assign({}, state, {
connections: [
...state.connections,
{
[actions.compositeKey]: action.connection
}
]
});
default:
return state;
}
}
export default reducer;
It might help to see it expressed this way too. It does the same thing but I think it reads a little nicer
const reducer = (state = {}, {type, compositeKey, connection}) => {
switch (type) {
case 'addConnection':
return Object.assign({}, state, {
connections: state.connections.concat({
[compositeKey]: connection
})
});
default:
return state;
}
}
export default reducer;
Or if you're using Immutable, something like this
import Immutable from 'immutable';
const reducer = (state = Immutable.Map(), {type, compositeKey, connection}) => {
switch (type) {
case 'addConnection':
return state.set(
'connections',
state.get('connections').concat({
[compositeKey]: connection
})
);
default:
return state;
}
}
export default reducer;
This may work
const reducer = (state = {}, {type, compositeKey, connection}) => {
switch (type) {
case 'addConnection':
var newData={};
newData[compositeKey]=connection;
return Object.assign({}, state, newData)
});
default:
return state;
}
}
export default reducer;