I've just started implementing Redux in a React application, and it's the first time i try to, so please bear with me.
My problem is that i can't access the data in my component this this.props.questions
I have a simple action which is supposed to async fetch some data
export function fetchQuestions(url) {
const request = axios.get('url');
return (dispatch) => {
request.then(({data}) => {
dispatch({ type: 'FETCH_QUESTIONS', payload: data });
console.log(data);
});
};
}
Which is picked up my reducer questions_reducer
export default function(state = [], action) {
switch(action.type) {
case 'FETCH_QUESTIONS':
console.log('Getting here');
return state.concat([action.payload.data]);
console.log('But not here');
}
return state;
}
My index reducer looks like this:
import { combineReducers } from 'redux';
import fetchQuestions from './question_reducer';
const rootReducer = combineReducers({
questions: fetchQuestions
});
export default rootReducer;
I pass it to my store where i apply the thunk middleware and finally into <Provider store={store}> which wraps my app, but the prop just returns undefined in my React component
configureStore:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState,
applyMiddleware(thunk)
);
}
I don't know if the console.log is to be trusted but it logs from my questions_reducer before the data is returned from the dispatch in my action
EDIT (Component)
class QuestionsRoute extends Component {
constructor(props) {
super(props);
this.state = {
};
}
componentDidMount() {
this.props.fetch('someUrl);
setTimeout(function(){ console.log(this.props.questions) },
1500);
}
render() {
{console.log(this.props.questions)}
return (
<div>
<1>Hello</1>
{this.props.questions !== undefined ?
<p>We like props</p>: <p>or not</p>
}
</div>
);
}
};
const mapStateToProps = (state) => {
return {
questions: state.questions,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetch: () => dispatch(fetchQuestions())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(QuestionsRoute);
In your reducer
export default function(state = [], action) {
switch(action.type) {
case 'FETCH_QUESTIONS':
return state.concat([action.payload.data]);
}
return state;
}
You should probably instead have return state.concat([action.payload]);
Since from dispatch({ type: 'FETCH_QUESTIONS', payload: data }); we see that payload is data, it doesn't contain it.
Update: I'd recommend setting up redux-devtools / redux-devtools-extension / react-native-debugger so you can visually see your actions and store state live - makes things like this a lot easier to debug!
Related
I am supposed to fetch data from an endpoint and display the results using Redux-Sauce.
All is fine except I can't seem to update the state after I fetch data. Read the docs so this is what I could come up with. Please tell me what I am doing wrong!?
How do I update the state calling the action creators inside HomeContainer.js?
Link to codeSandbox
https://codesandbox.io/s/fragrant-sky-56yhi?file=/src/index.js
HomeContainer.js
import React, { useEffect, useState } from "react";
import axios from "axios";
import { connect } from "react-redux";
import Creators from "../redux/Reducers/reducers";
const HomeContainer = ({ iTunesData, actions }) => {
const { loading, data, error } = iTunesData;
const [searchTerm, setSearchTerm] = useState("");
const submitHandler = (e) => {
e.preventDefault();
const getData = async () => {
actions.fetchDataRequest();
try {
const { data } = await axios.get(
`https://itunes.apple.com/search?term=${searchTerm}`
);
// console.log(data);
actions.fetchDataSuccess(data);
} catch (error) {
actions.fetchDataFail(error);
}
};
getData();
// console.log("On submit handler clicked!");
};
// console.log(iTunesData, actions);
// console.log(searchTerm);
// console.log(iTunesData);
console.log(loading, data, error);
return (
<form onSubmit={submitHandler}>
<h1> Home Container</h1>
<input
placeholder="Search..."
type="text"
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button>Go</button>
</form>
);
};
const mapStateToProps = (state, ownProps) => {
return {
iTunesData: state
};
};
const mapDispatchToProps = (state, ownProps) => {
return {
actions: Creators
};
};
export default connect(mapStateToProps, mapDispatchToProps)(HomeContainer);
reducer.js
import { createReducer, createActions } from "reduxsauce";
const { Types, Creators } = createActions({
fetchDataRequest: null,
fetchDataSuccess: ["payload"],
fetchDataFail: ["error"]
});
export default Creators;
const initialState = {
loading: false,
data: [],
error: false
};
export const fetchDataRequest = (state = initialState, action) => {
return { ...state, loading: true, data: [], error: false };
};
export const fetchDataSuccess = (state = initialState, action) => {
return { ...state, data: action.payload, error: false };
};
export const fetchDataFail = (state = initialState, action) => {
return { ...state, data: null, error: action.error };
};
// map our action types to our reducer functions
export const HANDLERS = {
[Types.FETCH_DATA_REQUEST]: fetchDataRequest,
[Types.FETCH_DATA_SUCCESS]: fetchDataSuccess,
[Types.FETCH_DATA_FAIL]: fetchDataFail
};
export const reducer = createReducer(initialState, HANDLERS);
store.js
import { applyMiddleware, combineReducers, compose, createStore } from "redux";
import { reducer } from "./Reducers/reducers";
import thunk from "redux-thunk";
const store = createStore(reducer, applyMiddleware(thunk));
export default store;
index.js
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./redux/store";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
rootElement
);
Your mapDispatchToProps is wrong. Written like you want to use it, it would need to acutally bind dispatch to the actions, which you don't.
If you want to use that nested, you will have to call bindActionCreators manually.
const mapDispatchToProps = (dispatch) => {
return {
actions: bindActionCreators(Creators, dispatch)
};
};
Otherwise you could also use the "object notation"
const mapDispatchToProps = Creators
in which case the bound action creators will be available as props.fetchDataSuccess, not props.actions.fetchDataSuccess.
Generally, it is also recommended to not use connect at all with function components, but the React-Redux hooks useSelector and useDispatch.
See https://react-redux.js.org/api/hooks
Also, as for your internship, please forward the official Redux Style Guide to your team, with best regards from a Redux Maintainer ;)
https://redux.js.org/style-guide/style-guide/
We really want them to use the official Redux Toolkit, as it will simplify their code a lot more than Redux-Sauce already does - including allowing for immutable logic in reducers thanks to immer integration and containing a full blown api cache abstraction.
Maybe trying that out and prototyping using it might make for a nice internship project for you in the end ;)
I have been following a guide to setup redux-thunk so I can fetch a users geolocation and then dispatch and update state. However, every time I attempt to dispatch the action with response data, it just sets the data to null.
When I attempt to simulate an API call with a timeout and set some random values, it works without a problem.
geoLocationActions.js
export function geoLocationActions() {
return dispatch => {
const geolocation = navigator.geolocation;
geolocation.getCurrentPosition((position) => {
console.log(position.coords);
dispatch({
type: 'FETCH_USER_LOCATION_SUCCESS',
payload: position
});
});
}
};
MapContainer.js
import React from "react";
import { geoLocationActions } from '../../../actions/geoLocationActions';
import { connect } from 'react-redux';
class MapContainer extends React.Component {
componentWillMount() {
this.props.geoLocationActions();
}
render() {
return (
<div>
<p>Fetching location...</p>
</div>
);
}
}
// update current geolocation state
const mapDispatchToProps = (dispatch) => {
return {
geoLocationActions: () => dispatch(geoLocationActions())
};
}
const mapStateToProps = (state) => {
return {
state: state
};
};
export default connect(mapStateToProps, mapDispatchToProps)(MapContainer);
reducers.js
case 'FETCH_USER_LOCATION_SUCCESS':
return {
...state,
userLocation: action.payload
}
store.js
import { createStore, combineReducers, applyMiddleware, compose } from
'redux';
import reducer from '../reducers/reducers';
import reduxThunk from "redux-thunk";
const rootReducer = combineReducers({
state: reducer
});
export const store = createStore(
rootReducer,
compose(
applyMiddleware(reduxThunk),
window.devToolsExtension ? window.devToolsExtension() : f => f
)
);
It turns an HTML5 Geoposition object. You need to convert it to a regular object that can be serialized with JSON.stringify.
You can use this method:
const geopositionToObject = geoposition => ({
timestamp: geoposition.timestamp,
coords: {
accuracy: geoposition.coords.accuracy,
latitude: geoposition.coords.latitude,
longitude: geoposition.coords.longitude
}
})
Update your geoLocationActions.js like this:
export function geoLocationActions() {
return dispatch => {
const geolocation = navigator.geolocation;
geolocation.getCurrentPosition((position) => {
const positionObj = geopositionToObject(position)
console.log(positionObj);
dispatch({
type: 'FETCH_USER_LOCATION_SUCCESS',
payload: positionObj
});
});
}
};
You can have a look at my repo to see the same code.
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'm trying to add Redux to my test app but I'm having trouble fetching data from my API. It feels like I've gone through all of the steps but I can't access the props from the fetch in my component, so I'm messing up somewhere along the way. My code:
actions/index.js:
import 'whatwg-fetch';
import ReduxThunk from 'redux-thunk';
export const FETCH_RECIPES = 'FETCH_RECIPES';
const ROOT_URL = 'http://myapi/recipe/';
export function fetchRecipes(id) {
const url = ROOT_URL + "0";
// const request = fetch(url);
return (dispatch) => {
fetch(url)
.then((response) => response.json())
.then((request) => dispatch(fetchRecipesSuccess(request)))
};
}
export function fetchRecipesSuccess(request) {
return {
type: FETCH_RECIPES,
request
};
}
reducer_recipe.js:
import { FETCH_RECIPES } from "../actions/index";
export default function(state = [], action) {
switch(action.type) {
case FETCH_RECIPES:
return [action.payload.data, ...state];
}
return state;
}
reducers/index.js:
import { combineReducers } from 'redux';
import RecipesReducer from './reducer_recipes';
const rootReducer = combineReducers({
recipes: RecipesReducer
})
export default rootReducer;
aaaaand in my component I'm using this code:
function mapStateToProps({ recipes }) {
return { recipes };
}
connect(mapStateToProps, {fetchRecipes})(Recipe);
And in my index.js I'm creating my store with const createStoreWithMiddleware = createStore(reducers, applyMiddleware(ReduxPromise));
With my thinking I should be good to use this.props to access the data I've fetched from my API but I guess I'm dropping the data somewhere along the way. What am I missing?
Check your reducer well. You seem to be returning action.payload.data whereas in your fetchRecipesSuccess, it's named request. And you can console.log the action object to see what you've got
import { FETCH_RECIPES } from "../actions/index";
export default function(state = [], action) {
switch(action.type) {
case FETCH_RECIPES:
// Verify here that your request object has data
return [...state, action.request.data];
// Default state
default:
return state;
}
}
Hope this helps!
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