I just started to experiment with react and redux and I face couple of issues on the way.
When I try to render async data on route change the dispatched action is getting fired twice. First is undefined and than comes the real data.
Here is my store
import { createStore, combineReducers, applyMiddleware } from 'redux'
import createLogger from 'redux-logger'
import thunk from 'redux-thunk'
import { routerReducer, routerMiddleware, push } from 'react-router-redux'
import reducers from '../reducers'
import { browserHistory } from 'react-router';
const middleware = [ thunk ];
if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger());
}
middleware.push(routerMiddleware(browserHistory));
// Add the reducer to your store on the `routing` key
const store = createStore(
combineReducers({
reducers,
routing: routerReducer
}),
applyMiddleware(...middleware),
)
export default store;
reducer
export const RESOLVED_GET_PROFILE = 'RESOLVED_GET_PROFILE'
const profileReducer = (state = {}, action) => {
switch (action.type) {
case 'SET_PROFILE':
return {profile: action.profile}
default:
return state;
}
};
export default profileReducer;
actions
import * as types from './actionTypes';
import Api from '../middleware/Api';
export function getProfile() {
return dispatch => {
dispatch(setLoadingProfileState()); // Show a loading spinner
Api.get('profile').then(profile => {
dispatch(doneFetchingProfile);
dispatch(setProfile(profile));
}).catch(error => {
dispatch(showError(error));
throw(error);
});
}
}
function setProfile(data) {
return {
type: types.SET_PROFILE,
profile: data
}
}
function setLoadingProfileState() {
return {
type: types.SHOW_SPINNER,
loaded: false
}
}
function doneFetchingProfile() {
return {
type: types.HIDE_SPINNER,
loaded: true
}
}
function showError() {
return {
type: types.SHOW_ERROR,
loaded: false,
error: 'error'
}
}
and here is my component
import React, {PropTypes, Component} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as profileActions from '../../../actions/profileActions';
class Profile extends Component {
static propTypes = {
profile: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
profile:{
username: '',
password: '',
email: ''
}
}
this.onUpdate = this.onUpdate.bind(this)
}
onUpdate(event) {
alert()
}
componentDidMount() {
//here I dispatch the action
this.props.actions.getProfile()
}
componentWillReceiveProps(nextProps) {
}
render() {
console.log(this.props)
//this.props.profile on first is undefined and then filled
const { profile } = this.props.profile
return (
<div>
</div>
);
}
}
function mapStateToProps(state) {
return {
profile: state.default.profile,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(profileActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Profile);
what do I wrong?
You said //this.props.profile on first is undefined and then filled
That's because in the first render, state.profile is undefined, until the request response arrives and the setProfile action is dispatched.
There's also the problem Andrew noted that you're calling dispatch(doneFetchingProfile). Since you're using redux-thunk, that will trigger calling doneFetchingProfile(dispatch, getState), but the action HIDE_SPINNER will never get dispatched.
UPDATE: There's nothing wrong with your code. You can see before SHOW_SPINNER the output of console.log(this.props) and there's no profile because there's no profile in state as well.
Then when your request succeeds, profile is set in state, then passed to your component and then you can see in the log that profile is set. Those are not actions dispatched, this is the log of your props.
The first time is undefined because the initial state declared in the reducer is {} (there's no profile here as well).
If you change
const profileReducer = (state = {}, action) => {
to
const profileReducer = (state = {profile: 'some initial value'}, action) => {
you'll see that the first console.log(this.props) will show profile with the value 'some initial value' and then change to the remote data.
This is what happening here
Your component render and show undefined on console because there is no profile data so far.
After component mount it call componentDidmount which fire an action to fetch data from url.
You get data from api and update the redux state which update your component as well.
Therefore you render function is called again and this time it shows the profile data.
There is nothing dispatching two times. The code is perfectly fine.
You dispatching 2 actions
dispatch(doneFetchingProfile);
dispatch(setProfile(profile));
First of them have no data, and it's look like tihs action set to state some data and update your component.
Related
I'm new in react-redux, and I have a problem with communication between reducer and store.
This is the idea on which I base:
I have a component "Login", that contains a button and two text inputs, and when i click that, I send the action to the reducer. Then, I update the state, and send it to the UI again (thats the way i understand the logic, correct me if i'm wrong). The problem occurs in the reducer, it never enter there, but yes in the action file (tested with console.logs). Maybe the connect is not working? or is in the store part?
Here I detach how I did it
action.js, with two operations only
const logout = () => {
return {
type: "USER_LOGOUT",
payload: false,
};
};
const login = () => {
return {
type: "USER_LOGIN",
payload: true,
};
};
export { logout, login };
reducer.js implementation, only change one boolean value
const initialState = {
logged: false,
};
export default (state = initialState, action) => {
if (action.type === "USER_LOGOUT") {
return {
...state,
logged: false,
};
}
if (action.type === "USER_LOGIN") {
return {
...state,
logged: true,
};
}
return state;
};
index.js (store), here's how i declare the store part
import { createStore, combineReducers } from "redux";
import loginReducer from "./reducer";
const reducers = combineReducers({
loginReducer,
});
const store = createStore(reducers);
export default store;
Login.js, only the touchable part
import { logout, login } from "../redux/actions";
import { connect } from "react-redux";
...
connect(null, { logout, login }, Login);
...
<TouchableOpacity>
...
onPress={() => checkValidation()}
...
</TouchableOpacity>
Here checkValidation, who calls the action "login"
checkValidation() =>
...
login();
...
You are not dispatching the action. To make Redux aware of an action you must dispatch it.
If you are using a class component you need to connect the component and pass it the dispatch action from redux.
I suggest you to use the hooks because its way easier.
1-Import the useDispatch hook
import { useDispatch } from "react-redux";
2-Create the dispatch:
const dispatch = useDispatch();
3-Dispatch your action:
checkValidation() =>
...
// Since your function login already returns the action object:
dispatch(login());
...
I am learning react-redux, so I decided to implement what I have been learning. But I am have a bug challenge. So I console.logged this.props.users from mapStateToProps function.
I believe there's something I not doing right which I don't understand. Please an explanation in other to move on. Thanks you so much for helping out.
Here is my code.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchUsers } from '../actions/userAction';
import UserList from '../components/UserList';
class UserPage extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.fetchUsers();
}
componentDidMount() {
console.log(this.props.users);
}
render() {
return (
<div>
<h2>Users Page</h2>
<UserList users={this.props.users} />
</div>
);
}
}
const mapStateToProps = state => {
return {
users: state.userReducer.users
};
};
const mapDispatchToProps = dispatch => {
return {
fetchUsers: () => dispatch(fetchUsers())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(UserPage);
So this is what I get from the chrome console - Empty arrays.
props showing empty arrays
But when I check the React DevTool and Redux DevTool, they display the expected Props and States respectively. Below are the snapshot of the dev tools
React devtool shows the correct Props
Redux devtool show the correct States and Actions
userAction.js
import axios from 'axios';
import * as types from './actionTypes';
export let fetchingUser = () => {
return {
type: types.FETCHING_USERS
};
};
export let fetchedUser = payload => {
return {
type: types.FETCHED_USER,
payload
};
};
export let fetchUser_error = () => {
return {
type: types.FETCH_USER_ERROR
};
};
export let fetchUsers = () => {
let url = 'https://eventcity.herokuapp.com/api/v1/users';
return dispatch => {
dispatch(fetchingUser());
return axios
.get(url)
.then(response => {
const users = response.data.data;
dispatch(fetchedUser(users));
})
.catch(err => {
dispatch(fetchUser_error());
});
};
};
userReducer.js
import * as types from '../actions/actionTypes';
import initialState from './initialState';
const userReducer = (state = initialState, action = {}) => {
switch (action.type) {
case types.FETCHING_USERS:
return { ...state, users: [], error: null, loading: true };
case types.FETCHED_USER:
return { ...state, users: action.payload, error: null, loading: false };
case types.FETCH_USER_ERROR:
return {
...state,
users: [],
error: { message: 'Error loading data from the API' },
loading: false
};
default:
return state;
}
};
export default userReducer;
configureStore.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from '../reducer/rootReducer';
const configureStore = () => {
return createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)));
};
export default configureStore;
rootReducer.js
import { combineReducers } from 'redux';
import userReducer from './userReducer';
const rootReducer = combineReducers({
userReducer
});
export default rootReducer;
I think you might want to check this
https://github.com/reactjs/react-redux/issues/129. Your problem is using componentDidMount and componentWillMount without having a better understanding of what they are used for.
The problem is not with redux, all you need to understand is that your fetchUsers request is async and componentDidMount function is only executed once after the component has rendered and it may so happen that the data is not present by the time componentDidMount function is executed and hence your console.log(this.props.users); return empty array, Log it in the render method and you will see the correct data
class UserPage extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.fetchUsers();
}
render() {
console.log(this.props.users);
return (
<div>
<h2>Users Page</h2>
<UserList users={this.props.users} />
</div>
);
}
}
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 have the mapStateToProps workflow down, but what if I want to respond to actions in a way that doesn't fit well into the state => props paradigm? For instance:
this.props.dispatch(saveArticle(...))
// on successful save, redirect to article page
If I'm just using regular old XHRs rather than actions, it would look something like this:
saveArticle(...).then(article => this.router.push(`/articles/${article.id}`))
It's not clear how this would fit in with the standard React/Redux workflow; I've seen people suggest that the saveArticle() action creator could fire off the router change, but I want to keep those separate; I should be able to save an article without being forced to redirect.
A workaround could be to do it in mapStateToProps; have the action set a flag or something, like articleWasSaved, and have the component that does the saving look for that prop and redirect if it sees it, but that seems really ugly, especially if multiple things are looking for that update, since it would likely require the component(s) to clear the flag.
Is there a simple/standard solution I'm missing?
Redux-thunk allows you to dispatch functions as actions. It is ideally to dispatch async operations.
Here I've created an example I think It will be useful for you:
actions.js
export const tryingAsyncAction = () => {
return {
type: 'TRYING_ASYNC_ACTION'
}
}
export const actionCompleted = () => {
return {
type: 'ACTION_COMPLETED'
}
}
export const errorAsyncAction = (error) => {
return {
type: 'ERROR_ASYNC_ACTION',
error
}
}
export function someAsynAction() {
return dispatch => {
dispatch(tryingAsyncAction())
ApiService.callToAsyncApi(...)
.then(() => {
dispatch(actionCompleted())
}, (cause) => {
dispatch(errorAsyncAction(cause))
})
}
}
reducer.js
const initialState = {
tryingAction: false,
actionCompleted: false,
error: null,
shouldRedirect: false,
redirectUrl: null
}
export default function reducer(state = initialState, action) {
switch (action.type) {
case 'TRYING_ASYNC_ACTION':
return Object.assign({}, state, {
tryingAction: true
})
case 'ACTION_COMPLETED':
return Object.assign({}, state, {
tryingAction: false,
actionCompleted: true,
shouldRedirect: true,
redirectUrl: 'someUrl'
})
case 'ERROR_ASYNC_ACTION':
return Object.assign({}, state, {
tryingAction: false,
actionCompleted: false,
error: action.error
})
default:
return state
}
}
Your createStore file
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk' //npm install --save redux-thunk
//Other imports...
const store = createStore(
reducer,
applyMiddleware(
thunkMiddleware
)
)
YourComponent.js
componentWillReceiveProps(nextProps){
if(nextProps.shouldRedirect && nextProps.redirectUrl)
this.router.push(`/articles/${article.id}`)
}
Let me know if there is something you dont understand. I will try to clarify
You could make use of react-thunk in this case.
actions/index.js
export function saveArticle(data) {
return (dispatch, getState) => (
api.post(data).then(response => {
dispatch({ type: 'SAVE_ARTICLE', payload: response })
return response;
})
)
}
reducer/index.js
import { combineReducers } from 'redux';
const initialState = {
list: [],
current: null,
shouldRedirect: false,
redirectTo: null
};
export function articles(state = initialState, action) {
switch(action.type) {
case 'SAVE_ARTICLE':
return {
shouldRedirect: true,
redirectTo: '/some/url',
current: action.payload,
list: [...state.list, action.payload]
};
default: return state;
}
}
export default combineReducers({ articles });
store/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
// Note: this API requires redux#>=3.1.0
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
component/index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as Actions from 'actions/index';
class MyComponent extends Component {
_handleSubmit = () => {
// get form values somehow...
// const values = getFormValues();
this.props.saveArticle(values).then(response => {
// you can handle you redirect here as well,
// since saveArticle is returning a promise
});
};
componentWillReceiveProps(nextProps) {
// you can handle the redirection here listening to changes
// on shouldRedirect and redirectTo that will be triggered
// when the action 'SAVE_ARTICLE' is dispatched
if(nextProps.shouldRedirect && nextProps.redirectTo) {
this.routes.push(nextProps.redirectTo);
}
}
render() {
// just an example
return (
<form onSubmit={this._handleSubmit}>
{ /* ... other elements here */ }
</form>
)
}
}
export default connect(
state => ({
articles: state.articles.list,
article: state.articles.current,
redirectTo: state.articles.redirectTo,
shouldRedirect: state.articles.shouldRedirect
}),
Actions
)(MyComponent);
PS: I'm using some babel syntax sugar here, so make sure you're the following presets are set in your .babelrc.
es2015
stage-2
stage-0
react
I try to deal with ajax data in my learning react,redux project and I have no idea how to dispatch an action and set the state inside a component
here is my component
import React, {PropTypes, Component} from 'react';
import Upload from './Upload';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as profileActions from '../../../actions/profileActions';
class Profile extends Component {
static propTypes = {
//getProfile: PropTypes.func.isRequired,
//profile: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
profile:{
username: '',
password: ''
}
}
this.onUpdate = this.onUpdate.bind(this)
}
onUpdate(event) {
alert()
}
componentWillMount() {
}
componentDidMount() {
}
render() {
const {profile} = this.props;
return (
<div>
</div>
);
}
}
function mapStateToProps(state) {
console.log(state)
return {
profile: state.default.profile,
};
}
function mapDispatchToProps(dispatch, ownProps) {
return {
actions: bindActionCreators(profileActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Profile);
I create the store as it follows
import { createStore, combineReducers, applyMiddleware } from 'redux'
import createLogger from 'redux-logger'
import thunk from 'redux-thunk'
import { routerReducer, routerMiddleware, push } from 'react-router-redux'
import reducers from '../reducers'
import { browserHistory } from 'react-router';
const middleware = [ thunk ];
if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger());
}
middleware.push(routerMiddleware(browserHistory));
// Add the reducer to your store on the `routing` key
const store = createStore(
combineReducers({
reducers,
routing: routerReducer
}),
applyMiddleware(...middleware),
)
export default store;
reducer
export const RESOLVED_GET_PROFILE = 'RESOLVED_GET_PROFILE'
const profileReducer = (state = {}, action) => {
switch (action.type) {
case 'RESOLVED_GET_PROFILE':
return action.data;
default:
return state;
}
};
export default profileReducer;
actions
import * as types from './actionTypes';
import Api from '../middleware/Api';
export function getProfile() {
return dispatch => {
dispatch(setLoadingProfileState()); // Show a loading spinner
Api.getAll('profile').then(profile => {
dispatch(doneFetchingProfile(profile));
}).catch(error => {
throw(error);
});
/*Api.fetch(`profile`, (response) => {
console.log(response)
dispatch(doneFetchingBook()); // Hide loading spinner
if(response.status == 200){
dispatch(setProfile(response.json)); // Use a normal function to set the received state
}else {
dispatch(error)
}
}) */
}
}
function setProfile(data) {
return {type: types.SET_PROFILE, data: data}
//return { type: types.SET_PROFILE, data: data };
}
function setLoadingProfileState() {
return {type: types.SHOW_SPINNER}
}
function doneFetchingProfile(data) {
console.log(data)
return {
type: types.HIDE_SPINNER,
profile: data
}
}
function error() {
return {type: types.SHOW_ERROR}
}
but I have no idea how would I dispatch action and update the state after getProfile action
You need to only dispatch your event RESOLVED_GET_PROFILE right after dispatching doneFetchingProfile, or simply listen RESOLVED_GET_PROFILE and hide spinner on reducing it.
Api.getAll('profile').then(profile => {
dispatch(doneFetchingProfile(profile));
dispatch(resoloveGetProfile(profile));
})
Actually you r doing everything right - so I didn't understand what is your question is, so if you meant something else - let me know, I`ll try to describe you.
About dispatch(resoloveGetProfile(profile));
There you dispatch action, which will update your state, simple as you do with some static state, I saw that you already have setProfile action, so you can change that line, to call your existed function.
dispatch(setProfile(profile))
Than you need to reduce your state in this action
case 'SET_PROFILE' : (action, state) => {...state, profile: action.data}
Than your state will change and your components will update. Note that your 'get profile method' better to call from componentDidMount to avoid freezing at rendering because of performing web request.