I am missing a key step/concept in how asyn calls work in react-redux.
Currently, I am able to make api calls (can also see the data being returned via chrome-dev-tools) BUT the response data isn't
reflected in the application state; meaning the state of the quiz object, which by default is an empty object, doesn't get updated.
My expectation is that when the asyn call resolves, I parse through the response in the quizReducer, and return a new state (not a mutated state) that reflects the response data.
Yet, each time the call is made, the updated state returns an empty quiz object (which is the initial state).
I know I am missing something but I can't figure out what; would really appreciate some pointers/explanation. Thank you
The app had an initialState/Preloaded state that looks like this:
export default {
quizzes: [
{id: 0, name: "quiz_name2", completed: false, selected: false},
{id: 1, name: "quiz_name2", completed: false, selected: false}
],
quiz: { questions: {} }
};
Setup for the reducer in question:
import initialState from './initialState';
import quizConstants from '../constants/quizConstants';
const actionTypes = quizConstants.actions
// when app loads, user has not selected any quiz therefore
// initialState is an empty object
// here action is the payload we get when a quiz is selected (see QUIZ_SELETED action creator)
export default function quiz(state=initialState.quiz, action) {
switch(action.type){
// when a quiz is selected, return new state
case actionTypes.SELECT_QUIZ:
return Object.assign(
{},
state,
{
id: action.quiz.id,
name: action.quiz.name,
completed: action.quiz.completed,
selected: !action.quiz.selected,
fetching: action.quiz.fetching,
fetched: action.quiz.fetched,
questions: action.quiz.questions
}
)
case actionTypes.REQUEST_QUIZ:
return Object.assign(
{},
state,
{
id: action.quiz.id,
name: action.quiz.name,
completed: action.quiz.completed,
selected: !action.quiz.selected,
fetching: !action.quiz.fetching,
fetched: action.quiz.fetched,
questions: action.quiz.questions
}
)
case actionTypes.RECEIVE_QUIZ:
return Object.assign(
{},
state,
{
id: action.quiz.id,
name: action.quiz.name,
completed: action.quiz.completed,
selected: !action.quiz.selected,
fetching: action.quiz.fetching,
fetched: !action.quiz.fetched,
quiz: action.quiz.questions
}
)
default:
return state
}
};
index.js (rootreducer):
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import quizzes from './quizzesReducer'
import quiz from './quizReducer';
export default combineReducers({
quizzes,
quiz,
routing: routerReducer
});
QuizActionCreators
import quizConstants from '../constants/quizConstants';
import { quizzesEndPoint } from '../constants/appConstants';
import axios from 'axios';
const actionTypes = quizConstants.actions
// select a quiz
export const selectQuiz = (quiz) => {
return {
type: actionTypes.SELECT_QUIZ,
quiz
}
};
const receiveQuiz = (quiz, data) => {
return {
type: actionTypes.RECEIVE_QUIZ,
quiz,
data
}
};
// call when componentWillMount
export const fetchQuiz = (quiz) => {
console.log("Make an api request here")
const url = quizzesEndPoint.concat(quiz.name)
axios.get(url)
.then(response => response.data)
.then(data => receiveQuiz(quiz, data))
}
export default { selectQuiz, fetchQuiz};
In your QuizActionCreator, your fetchQuiz is calling receiveQuiz and passing quiz and data as your parameter which the latter has the data from the server. I don't see any part in your reducers where you are setting the action.data to the state.
Try adding handler for RECEIVE_QUIZ inside your quizzesReducer and return the action.data to the state.
//quizzesReducer.js
export default function (state = initialState.quizzes, action){
...
if (action.type === actionTypes.RECEIVE_QUIZ) {
return action.data
}
...
};
Related
I am using JWT auth, when the user log in I store the token in the localstorage. How do I fetch the api with that token, so I can get the user details when the page
loads for the first time.
I'm already using React Thunk for the async requests but I don't know how to set the initialState with an async request. However is it okay to set the localstorage in the reducers?
You would want to do something like this in your action:
import axios from 'axios';
export const LOADING = "LOADING";
export const SUCCESS = "SUCCESS";
export const FAILURE = "FAILURE";
export const UPDATE = "UPDATE";
export const SUCCESSFUL_UPDATE = "SUCCESSFUL_UPDATE";
export const getSmurfs = () => dispatch => {
dispatch({ type: LOADING })
axios.get('http://localhost:3333/smurfs')
.then(res => dispatch({ type: SUCCESS, payload: res.data}))
.catch(err => dispatch({ type: FAILURE, payload: err}))
}
So you would start with a state of Loading which would change to Success or Failure depending on the response. Then in your reducer you would want to do something like:
import { LOADING, SUCCESS, FAILURE, UPDATE, SUCCESSFUL_UPDATE } from '../actions/index';
const initialState = {
smurfs: [],
loading: false,
error: "",
updateID: "",
clicked: false,
update: []
}
export default function reducer(state= initialState, action) {
switch(action.type) {
case LOADING:
return {
...state,
smurfs: [],
loading: true,
err: ''
}
case SUCCESS:
return {
...state,
smurfs: action.payload,
loading: false,
err: ''
}
Basically when it is successful it will turn off the loading and display your returned data
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getSmurfs, deleteSmurf, update } from '../actions/index';
import Smurfs from './smurfs';
import Form from './form';
import UpdateForm from './updateForm';
class SmurfsViewer extends Component {
componentDidMount() {
this.props.getSmurfs()
}
render() {
console.log(this.props.smurfs)
// if loading returns true then display loading smurfs..
if(this.props.loading) {
return (<h1>LOADING SMURFS....</h1>)
}
//if clicked resolves true then display the form to allow updating of the smurf that had its edit button clicked
let form;
if(this.props.clicked) {
form = <UpdateForm />
} else {
form = <Form />
}
return(
<div>
<Smurfs smurfs={this.props.smurfs} deleteSmurf={this.props.deleteSmurf} update={this.props.update}/>
{form}
</div>
)
}
}
const mstp = state => {
console.log("FROM VIEWER:", state)
return {
smurfs: state.smurfs,
loading: state.loading,
clicked: state.clicked
}
}
export default connect(mstp, { getSmurfs, deleteSmurf, update })(SmurfsViewer);
So you need to send the state from Redux through the mapStateToProps(mstp) and connect methods. Then you can use them in the component and it will update your redux state as needed. Then just refer to them as this.props.getSmurfs or something along those lines
I have created a basic reducer the state do get updated but render method does not get called here is the reducer file.
reducer.js
const accountSelector = (
store = {
items: [],
selectAll: false,
searchList: [],
filterList: [],
labelCount: 0
},
action
) => {
let newStore = {};
switch (action.type) {
case 'INITIALIZE_LIST':
console.log('in INITIALIZE_LIST');
newStore = { ...store, items: action.payload };
break;
case 'SEARCH_LIST':
newStore = { ...store, searchList: action.payload };
break;
case 'FILTER_LIST':
newStore = { ...store, filterList: action.payload };
break;
case 'SELECT_ALL_ACCOUNTS':
newStore = { ...store, selectAll: !store.list.selectAll };
break;
case 'UPDATE_LABEL_COUNT':
newStore = { ...store, labelCount: action.payload };
break;
default:
newStore = { ...store };
break;
}
console.log(' newStore: ', newStore);
return newStore;
};
export default accountSelector;
The state does get updated as I already logged it.
As you can see there are no mutation in reducer and still the render method does not get called.
Here are mapStateToProps and mapDispatchToProps
const mapStateToProps = state => ({
accountSelector: state.accountSelector
});
const mapDispatchToProps = dispatch => ({
initializeAccountsList: list => {
console.log('~~~~ ac selector ~~~~~~ in initializeAccountsList method');
dispatch({
type: 'INITIALIZE_LIST',
payload: list
});
}
Updated The question with store.js file where I combine reducer:
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import promise from 'redux-promise-middleware';
import group from './reducer/groupReducer';
import clients from './reducer/clientReducer';
import accounts from './reducer/accountReducer';
import accountSelector from './reducer/accountSelectorReducer';
import createfeed from './reducer/createfeed';
const middleware = applyMiddleware(thunk, promise());
const reducers = combineReducers({
createfeed,
group,
clients,
accounts,
accountSelector
});
export default createStore(reducers, {}, middleware);
UPDATE: code for render method that has component that uses one of the state properties of accountSelector
render() {
let list = this.props.accountSelector.items;
if (this.props.accountSelector.searchList.length > 0) {
list = this.props.accountSelector.searchList;
}
return (
<div className="accountv2-select">
<UserList
updateLabelCount={this.updateLabelCount}
list={list}
selectedAccounts={this.props.selectedAccounts}
selectAll={this.props.selectAll}
selectedLoginIds={this.props.selectedLoginIds}
/>
);
}
Ofcourse the list will get the default value of items i.e is an empty array already defined in the reducer since the render method is not called even though state gets updated within reducer.
Removed old answer, as it's irrelevant now
It looks like your store.js file is combining reducers correctly, your mapStateToProps and mapDispatchToProps functions look fine, and your accountSelector reducer file looks fine, so at this point the only thing left is actually using your redux store data (from mapStateToProps) in your component.
You should be accessing state date (like items, searchList, filterList, ect.) like this:
// you mapped your entire accountSelector state to this.props.accountSelector
this.props.accountSelector.items
this.props.accountSelector.searchList
this.props.accountSelector.filterList
// ... etc
I am working on a Redux project where I am trying to retrieve the values from an API server using axios library.After retrieving the values from the server,I am trying to save it in the application state.I am doing the API call in my Actions.The Actions.js file is as shown below:
import axios from 'axios';
export const FETCH_POSTS = 'fetch_posts';
let token = localStorage.token
if(!token)
token = localStorage.token = Math.random().toString(36).substr(-8)
const API = 'http://localhost:3001';
const headers = {
'Accept' : 'application/json',
'Authorization' :'token'
}
export function fetchPosts() {
const URL = `${API}/posts`;
const request = axios.get(URL,{headers});
return dispatch => {
request.then(({data}) => {
dispatch({
type : FETCH_POSTS,
payload : data
})
})
}
}
After retrieving the data,I am trying to console.log the object returned in my Component.My Component looks like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchPosts } from '../actions';
import _ from 'lodash';
class PostsIndex extends Component {
componentDidMount() {
this.props.fetchPosts();
}
render() {
console.log(this.props.posts); //returns an empty object
return(
<div>
Posts
</div>
);
}
}
function mapStateToProps(state) {
return { posts: state.posts };
}
export default connect(mapStateToProps, { fetchPosts })(PostsIndex);
The object that I am trying to retrieve from the API server is given below:
const defaultData = {
"8xf0y6ziyjabvozdd253nd": {
id: '8xf0y6ziyjabvozdd253nd',
timestamp: 1467166872634,
title: 'Udacity is the best place to learn React',
body: 'Everyone says so after all.',
author: 'thingtwo',
category: 'react',
voteScore: 6,
deleted: false,
commentCount: 2
},
"6ni6ok3ym7mf1p33lnez": {
id: '6ni6ok3ym7mf1p33lnez',
timestamp: 1468479767190,
title: 'Learn Redux in 10 minutes!',
body: 'Just kidding. It takes more than 10 minutes to learn technology.',
author: 'thingone',
category: 'redux',
voteScore: -5,
deleted: false,
commentCount: 0
}
}
Now,I can see the object with the 2 values returned from the API server in my network response.But,if I try to console.log the value of the same posts(The result of the api call is saved as "posts" state in the store), it returns an empty object.What am I doing wrong, can anybody please help me with this?
Reducer files
index.js
import { combineReducers } from 'redux';
import PostReducer from './PostsReducer';
const rootReducer = combineReducers({
loading: false,
posts: PostReducer
});
export default rootReducer;
PostReducer.js
import _ from 'lodash';
import { FETCH_POSTS } from '../actions';
export default function(state = {}, action) {
switch (action.type) {
case FETCH_POSTS:
return _.mapKeys(action.payload.data, 'id');
default:
return state;
}
}
The thing is, you aren't rerendering PostsIndex, after fetchPosts finishes so you're always seeing an empty object. You cannot see the updated Redux store value unless you rerender your component. React Redux does not do this for you. Use state to trigger a rerender, such as a loading indicator:
componentDidMount() {
this.props.dispatch(fetchPosts())
.then(() => {
this.setState({
loading: false
});
});
}
render() {
return (
<div>
this.state.loading ?
/* loading UI could go here */
:
/* you can access this.props.posts here */
<div>
);
}
And I wouldn't use mapDispatchToProps in connect. Change your connect line to this:
export default connect(mapStateToProps)(PostsIndex);
This is because dispatch allows for promise chaining with Redux Thunk.
Once your post fetching finishes, the promise will resolve and state will be set (assuming Redux Thunk). This state setting will rerender the component and display the fetched posts.
I just started learning React with Redux, so please, be lenient.
I followed the Redux kinda getting started, and so far, so good. But following along 'basic' and 'advanced' redux doc sections, I'm ending in a position where all my components are isolated. Which is perfectly fine. Except that one of my component is designed to display notifications, dismiss and remove them.
The other one is a modal which needs 4 parameters to work : one with the header text section, same for the body, one boolean to set if its displayed or not, and one to trigger a confirm action when confirm button is clicked.
My notifications component needs the modal to display when the user wants to remove a notification. Both components have a state handled with redux.
Here are some pieces.
Below, the notifications reducer, which handles dismissing deleting and fetching the notifications. Quite simple.
// The notifications reducer
let initialState = {
isFetching: false,
didInvalidate: false,
notifications: []
}
const notifications = (state = initialState, action) => {
switch (action.type) {
case 'DISMISS':
return {...state, notifications: state.notifications.map(element => element.id === action.id ? {...element, dismissed: !element.dismissed} : element)}
case 'DISMISS_ALL':
return {...state, notifications: state.notifications.map(element => { return {...element, dismissed: false} }) }
case 'DELETE':
return {...state, notifications: state.notifications.filter(element => element.id !== action.id)}
case 'DELETE_ALL':
return {...state, notifications: []}
case 'INVALIDATE':
return {...state, didInvalidate: true}
case 'REQUEST':
return {...state, isFetching: true, didInvalidate: false}
case 'RECEIVE':
return {...state, isFetching: false, didInvalidate: false, notifications: action.posts, lastUpdate: action.receivedAt}
default:
return state
}
}
export default notifications
Then the notifications actions. The focus to me here, is on the deleteNotification and the deleteAllNotificationsactions which must bind the modal opening and wait for its response, then trigger the removing, and only after that.
Those ones would need (I'm guessing) to work like the fetchNotifications action, calling another action (modal's one), instead of making a data fetching like this is the case for fetchNotifications.
// The notifications actions
//#flow
import axios from 'axios'
/**
* NOTIFICATIONS ACTION CREATORS
*/
export const dismissNotification = (id: number) => {
return { type: 'DISMISS', id }
}
export const dismissAllNotifications = () => {
return { type: 'DISMISS_ALL' }
}
export const deleteNotification = (id: number) => {
return { type: 'DELETE', id }
}
export const deleteAllNotifications = () => {
return { type: 'DELETE_ALL' }
}
/**
* DATABASE NOTIFICATIONS ACTION CREATORS
*/
export const invalidate = (uri: string) => {
return { type: 'INVALIDATE', uri }
}
export const requestNotifications = (uri: string) => {
return { type: 'REQUEST', uri }
}
export const receiveNotifications = (uri: string, json: string) => {
return {
type: 'RECEIVE',
uri,
posts: json,
receivedAt: Date.now()
}
}
export const fetchNotifications = (uri: string) => {
return (dispatch: any) => {
dispatch(requestNotifications(uri))
return axios.get(uri)
.then(
response => response.data,
error => console.log('Error', error) // TODO logger les erreurs
)
.then(json => {
dispatch(receiveNotifications(uri, json))
})
}
}
Finally, the modal's reducer. The point here for the reducer is to handle the displaying (visible or not), the texts in it via headerMessage and bodyMessage.
The id is here to tell the modal if it has to dispatch a delete on a signle element or a whole bunch of it. And this is dirty to me. It should just return a confirm for instance, the one which will be catched by the notifications action as a response.
let initialState = {
showModal: false,
headerMessage: '',
bodyMessage: '',
id: false
}
const modal = (state = initialState, action) => {
switch (action.type) {
case 'MODAL_DISPLAY':
return {...state,
showModal: action.showModal,
headerMessage: action.headerMessage,
bodyMessage: action.bodyMessage,
id: action.id
}
case 'MODAL_HIDE':
return {...state,
showModal: action.showModal,
headerMessage: action.headerMessage,
bodyMessage: action.bodyMessage,
id: action.id
}
default:
return state
}
}
export default modal
Just to be clear, the modal component and the notifications component are living in different directories. I want the modal component to be usable in any context. Here this is about notifications, but if I want it to work with any other component than the notifications one, lets say the user's profile, I souldn't be tied to any component specifically.
In my case, the notifications component is the one which call the modal. This is it that 'drives' the global behaviour. But I guess I didn't build all this that well. Here is my notifications component index :
//#flow
import React from 'react'
import { render } from 'react-dom'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux'
import { fetchNotifications } from './store/actions/NotificationsActions'
import NotifiationsApp from './store/reducers/reducerCombiner'
import App from './components/App'
import ModalContainer from '../modal-component/store/containers/ModalContainer'
const loggerMiddleware: any = createLogger()
const composeEnhancers: any = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store: any = createStore(
NotifiationsApp,
composeEnhancers(
applyMiddleware(
thunkMiddleware,
loggerMiddleware
)
)
)
store.dispatch(fetchNotifications('/app_dev.php/notifications/load?offset=0&limit=10'))
render(
<Provider store={store}>
<div>
<App/>
<ModalContainer />
</div>
</Provider>,
document.getElementById('notifications-component')
)
As you can see, I'm calling the ModalContainer which is in facts, the 'smart' modal component abstraction.
And finally, here is my bad ; the ModalContainer which ties itself to the notifications component, where I'd like to 'return' the confirmation event when onConfirm is fired :
import { connect } from 'react-redux'
import { deleteAllNotifications, deleteNotification } from '../../../notifications-component/store/actions/NotificationsActions'
import { hideModal } from '../actions/ModalActions'
import ModalInstance from '../../components/Modal'
const getModalProperties = modal => {
return modal
}
const mapStateToProps = state => {
return getModalProperties(state.modal)
}
const mapDispatchToProps = dispatch => {
return {
onCancel: () => {
dispatch(hideModal(false, false, '', ''))
},
onConfirm: id => { // BELOW, THIS IS VERY BAD!!
id ? dispatch(deleteNotification(id)) : dispatch(deleteAllNotifications())
dispatch(hideModal(false, '', '', false))
}
}
}
const ModalDisplay = connect(
mapStateToProps,
mapDispatchToProps
)(ModalInstance)
export default ModalDisplay
I'm struggling on this one. Please, help!
I'm not sure whether I understood correctly, but it seems you're looking for ownProps. You want to pass a function to your ModalWindow component, which shall be called after the confirmation function has been called, right? So that you have an interface like
<ModalContainer onConfirm={myHandler}/>
In this case, your mapDispatchToProps needs to consider the props you passed to the container, like
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onCancel: () => {
dispatch(hideModal(false, false, '', ''))
},
onConfirm: id => { // BELOW, THIS IS VERY BAD!!
id ? dispatch(deleteNotification(id)) : dispatch(deleteAllNotifications())
dispatch(hideModal(false, '', '', false));
ownProps.onConfirm && ownProps.onConfirm(id);
}
}
}
You're on the right track in general. Conveniently, I just published Practical Redux, Part 10: Managing Modals and Context Menus, which demonstrates how to build and control modals, context menus, and notifications using Redux. That includes how to handle "return values" from modals as they're closed.
To specifically answer your question: the handling of the ID for clearing one notification vs all of them doesn't seem "dirty" or "bad" to me at all - that approach should be fine.
Looks like my reducer is not updating the store. Any idea why that would be happening?
import 'babel-polyfill'
const initialState = {
user: {
name: 'Username',
Avatar: '/img/default/avatar.png'
},
friendsList: []
}
export default (state = initialState, action) => {
switch (action.type) {
case 'setUserInfo' :
// If I console.log action.user here, I see that I'm getting good user data
return Object.assign({}, state, {
user: action.user
})
default: return state
}
}
Your reducer is not having any problem it will update the state and returning the correct one.
Please make sure that you have the following codes present.
create an action creator like this
var updateUser = function (user) {
return {
type: 'setUserInfo',
user: user
}
}
Create a store importing your reducer
import { createStore } from 'redux'
import userReducer from 'your reducerfile'
let store = createStore(userReducer)
You must need to dispatch the action by
store.dispatch(updateUser({name: 'some', Avatar: '/image/some'}));