I'm learning redux-saga and having a problem with calling my api.
It seems like my saga is not triggered and my props stay undefined.
I'm using fake-server for mocking data.
I tested the server and it seems to be working fine outside the saga.
My code looks like this:
UserView.js
const UserView = (props) => {
const {user} = props;
return (
<Header as='h2'>{user.name}</Header>
);
}
export default UserView
actions.js
export function showUserRequest(){
return{
type: USER_REQUEST
}
}
export function showUserSuccess(user) {
return {
type: USER_SUCCESS,
payload: user
}
}
export function showUserError(error){
return{
type: USER_ERROR,
payload: error
}
}
reducer.js
export default function (state = initialState, action) {
switch(action.type){
case USER_REQUEST:
return{
...state,
requesting: true,
successful: false,
errors: []
}
case USER_SUCCESS:
return{
...state,
user: action.payload,
requesting: false,
successful: true,
errors: [],
}
case USER_ERROR:
return{
requesting: false,
successful: false,
errors: state.errors.concat[{
body: action.error.toString(),
time: new Date(),
}],
}
default:
return state;
}
}
user.js
class User extends Component {
constructor(props) {
super(props);
this.fetchUser = this.fetchUser.bind(this);
}
componentWillMount(){
this.fetchUser();
}
fetchUser(){
const { showUserRequest } = this.props;
return showUserRequest;
}
render() {
const user = this.props;
return (<UserView user = {user.user}/>);
}
}
function mapStateToProps(state) {
return {
user: state.user
};
}
export default connect(mapStateToProps, {showUserRequest})(User);
sagas.js
const api = 'http://localhost:8080/user';
function userRequestApi () {
return axios.get(api)
}
function* userRequestFlow() { //does not seem to get invoked
try {
const user = yield call(userRequestApi);
yield put(showUserSuccess(user))
} catch (error) {
yield put(showUserError(error))
}
}
function* userWatcher() { //seems to get invoked
yield takeLatest(USER_REQUEST, userRequestFlow);
}
export default userWatcher
indexSaga.js/indexReducer.js
export default function* IndexSaga() {
yield all([
SignupSaga(),
LoginSaga(),
UserSaga()
]);
}
const IndexReducer = combineReducers({
route: routerReducer,
form: formReducer,
user: userReducer
})
export default IndexReducer
It seems like the userRequestFlow function is not called at all.
I'm sure it's just a silly rookie mistake but I just can't figure it out.
What am I doing wrong?
In your user.js you need to define mapDispatchToProps() and dispatch the showUserRequest action like this:
function mapDispatchToProps(dispatch) {
return {
dispatchUserRequest: () => dispatch(showUserRequest()),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(User);
And then invoke this.props.dispatchUserRequest() when you wish to dispatch the action.
Related
I'm a newbie in redux and react.js,
I am trying to make a button disappear on a component in react.js by putting an if condition on the state variable (articlesTable/index.js), which is connected to the redux library function on another file (actions/actionArticles.js), when a button on articlesTable/index.js is clicked, the component is connected with actions/actionArticles.js and dispatch a function in actions/actionArticles.js, which is called loadMoreData().
The function I am trying to configure the state in redux is,
in articlesActions.js
export const loadMoreArticles = () => async (dispatch, getState) => {
const lastArticleKey = Object.keys(getState().articlesMap).pop();
const lastArticle = getState().articlesMap[lastArticleKey];
console.log("articleMap", getState().articlesMap);
console.log("Last article", lastArticleKey, lastArticle);
let filteredArticles = {};
const uid = getState().auth.uid;
const userLevel = getState().profile.userLevel;
} else {
const filteredArticlesArray = [];
var lastArticleReached = false;
...
var lastArticleInArray = filteredArticlesArray[filteredArticlesArray.length-1];
if (lastArticleInArray[0]===lastArticleKey) {
console.log("Bingo, last article reached!");
lastArticleReached = true;
}
else if (lastArticleInArray[0]!== lastArticleKey)
{
console.log("Not last article");
lastArticleReached = false;
}
filteredArticles = Object.fromEntries(filteredArticlesArray.reverse());
}
dispatch({type: LAST_ARTICLE_REACHED, payload: lastArticleReached})
...
};
I dispatch this function with
dispatch({ type: LOAD_MORE_ARTICLES, payload: filteredArticles });
in the code snippet above
The root reducer looks like this,
reducers/index.js
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
import articlesStatusReducer from './articlesStatusReducer';
const rootReducer = combineReducers({
...
articlesStatus: articlesStatusReducer,
form: formReducer,
...
});
export default rootReducer;
In articleStatusReducer,
import {LAST_ARTICLE_REACHED} from "../actions/types";
export default function(state = {}, action) {
switch(action.type) {
case(LAST_ARTICLE_REACHED):
console.log(action.payload);
return action.payload;
default:
return state;
}
}
In the articlesTable/index.js, I connect like this
const mapStateToProps = (state) => {
return {
articlesMap: state.articlesMap,
appStatus: state.appStatus,
profile: state.profile,
lastArticleReached: state.articlesStatus,
}
};
const mapDispatchToProps = (dispatch) => {
return {
getArticlesWithData: () => dispatch(getArticlesWithData()),
loadMore: () => dispatch(loadMoreArticles())
}
};
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(ArticlesTable)
For some reason, articleStatus isn't recognised and when I do
console.log(this.props.articleStatus)
state.articleStatus is undefined
How can I reference state.articleStatus which should be boolean ?
Edit:
For some reason when I put it in a conditional JSX brackets in the render method, it prints out undefined
render () => {
{
console.log(this.props.lastArticleReached),
!this.props.lastArticleReached
: <Button> </Button>
?
<div><div>
}
}``
In function mapStateToProps, you should map state.articleStatus to a props.
somethings like this:
const mapStateToProps = (state) => {
return {
articlesMap: state.articlesMap,
appStatus: state.appStatus,
profile: state.profile,
lastArticleReached: state.articlesStatus,
articleStatus: state.articleStatus
}
};
So this.props.articleStatus will works . :)
The problem is in your reducer. Each case of your reducer must return the state but in your case, your return action.payload.
try something like this.
case(LAST_ARTICLE_REACHED):
console.log(action.payload);
return {...state, articleStatus: action.payload};
like this, articlesStatus became an object with one props, articleStatus, your boolean.
I tried another name for the props but with similar method as Thomas Caillard,
Reducer.js
case(REACH_LAST_ARTICLE):
return {...state, lastArticleReached: action.payload}
in component index.js
const mapStateToProps = (state) => {
return {
...
lastArticleReached: state.articlesMap.lastArticleReached
...
}
};
Thanks for all the helps so far
I am currently using React-Redux but for a pretty simple app.
The app just simply takes a user ID, password, and an address of a server that the user wants to get into. It gets into the server and runs a script in the server. But the functionality of the app is not important in my question.
I only need 3 states (username, password, and server_address) for the app.
However, I have three different reducers and actions that do the same thing just with the different state.
For example,
userReducer.js
// reducer functions takes a default state and an action to apply
import { UPDATE_USER } from '../actions/userActions'
export default function userReducer(state = '', { type, payload }) {
switch (type) {
case UPDATE_USER:
return payload;
default:
return state;
}
}
passwordReducer.js
// reducer functions takes a default state and an action to apply
import { UPDATE_PASSWORD } from '../actions/passwordActions'
export default function passwordReducer(state = '', { type, payload }) {
switch (type) {
case UPDATE_PASSWORD:
return payload;
default:
return state;
}
}
routerReducer.js // this is the server
// reducer functions takes a default state and an action to apply
import { UPDATE_ROUTER } from '../actions/routerActions'
export default function routerReducer(state = '', { type, payload }) {
switch (type) {
case UPDATE_ROUTER:
return payload;
default:
return state;
}
}
and actions that look like this:
export const UPDATE_PASSWORD = 'updatePassword'
export function updatePassword(newPassword) {
return {
type: UPDATE_PASSWORD,
payload: {
'newPassword': newPassword
}
}
}
It's same for the other two with the different variable.
Then in my component, I just connected mapActionsToProps to the component and put 3 functions that does the same thing (updating the state)
class Container extends React.Component {
constructor(props) {
super(props)
}
onUpdateUser = (e) => {
this.props.onUpdateUser(e.target.value)
}
onUpdatePassword = (e) => {
this.props.onUpdatePassword(e.target.value)
}
onUpdateRouter = (e) => {
this.props.onUpdateRouter(e.target.value)
}
...
using it like
This kinda works, but I am not sure if this is the right way to use React-Redux. First of all, they are duplicates and do not seem like a good practice. However, I can't think of a way to update each state in a React-Redux way without just putting similar codes.
Any help?
You could pass the event to your action.js
export const onInputChange = event => ({
type: 'UPDATE_INPUT',
payload: event
});
And simply grab the name and the value of the event in your reducer.js
const INITIAL_STATE = {
user: '',
password: ''
}
export const inputReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case: 'UPDATE_INPUT':
return { ...state, [action.payload.target.name]: action.payload.target.value };
default:
return state;
};
};
Then in your component
// ...
handleChange(event) {
this.props.onInputChange(event);
};
// ...
<input type='text' name='user' onChange={this.handleChange} />
You can use a single function just to send the key/value pairs you want to update.
export const UPDATE_USER_VALUE = 'updateUserValues'
export function updateUser(payload) {
return {
type: UPDATE_USER_VALUE,
payload: payload,
}
}
You must call that function like this:
onUpdateUser = (e) => {
this.props.onUpdateUser({
key: 'name',
value: e.target.value
})
}
onUpdatePassword = (e) => {
this.props.onUpdateUser({
key: 'password',
value: e.target.value
})
}
Then just update the values.
import { UPDATE_USER_VALUE } from '../actions/userActions'
const defaultState = {
username = '',
password = '',
server_address = ''
};
export default function passwordReducer(state = defaultState, { type, payload }) {
switch (type) {
case UPDATE_USER_VALUE:
return {
...state,
state[payload.key]: payload.value
};
default:
return state;
}
}
In the last couple of days I have been working on my Redux api call. I am actually having a problem getting the data back to the view component. Currently I'm able to see the data in the in the action generator, so I know at least I'm able to get it. However, nothing is showing in the view. I imagine it may have something to do with when it's loading. This is why I tried to load it when the component is rendering.
https://djangoandreact.herokuapp.com/user/1 is what is not loading.
codesandbox: https://codesandbox.io/s/zlor60q3jm?from-embed
Should be able to go to /user/1 at the end similar to going to /1 brings up an article(Tough Hope)
Heres the view component:
import React from "react";
import { connect } from "react-redux";
import { fetchUser } from "../store/actions/userActions";
class UserDetailView extends React.Component {
componentDidMount() {
const userID = this.props.match.params.userID;
fetchUser(userID); //fixed
}
render() {
const { user } = this.props.user;
console.log(user);
return (
<div>
<h3>{user.username}</h3>
</div>
);
}
}
const mapStateToProps = state => ({
user: state.user
});
const mapDispatchToProps = (dispatch, ownProps) => ({
fetchUser: dispatch(fetchUser(ownProps.match.params.userID))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserDetailView);
Action generator
import axios from "axios";
import { thunk } from "react-redux";
export function fetchUser(userID) {
console.log(userID);
return dispatch => {
return axios.get(`/api/user/${userID}`).then(res => {
dispatch(fetchUserSuccess(res.data));
console.log(res.data); // loads data
});
};
}
// Handle HTTP errors since fetch won't.
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
export const FETCH_USER_BEGIN = "FETCH_USER_BEGIN";
export const FETCH_USER_SUCCESS = "FETCH_USER_SUCCESS";
export const FETCH_USER_FAILURE = "FETCH_USER_FAILURE";
export const fetchUserBegin = () => ({
type: FETCH_USER_BEGIN
});
export const fetchUserSuccess = user => ({
type: FETCH_USER_SUCCESS,
payload: { user }
});
export const fetchUserFailure = error => ({
type: FETCH_USER_FAILURE,
payload: { error }
});
Reducers(which are probably fine):
import {
FETCH_USER_BEGIN,
FETCH_USER_SUCCESS,
FETCH_USER_FAILURE
} from "../actions/actionTypes";
const initialState = {
user: {},
loading: false,
error: null
};
export default function userReducer(state = initialState, action) {
switch (action.type) {
case FETCH_USER_BEGIN:
return {
...state,
loading: true,
error: null
};
case FETCH_USER_SUCCESS:
return {
...state,
loading: false,
user: action.payload.user
};
case FETCH_USER_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
user: {}
};
default:
return state;
}
}
folks. I found it.
case FETCH_USER_SUCCESS:
return {
...state,
loading: false,
user: action.payload.user
};
user is supposed to be user:action.payload
Also, the user action was supposed to be
export const fetchUserSuccess = user => ({
type: FETCH_USER_SUCCESS,
payload: user
})
WOOOOW. But, honestly, I learned so much about Redux in the last two sleepless nights, it was worth the pain. Really was. Now, instead of copy pasta, I know what an action generator is and does, and reducer (obvi)
I have a problem "Actions must be plain objects. Use custom middleware for async actions."
I'm using reactjs with this boilerplate (https://github.com/react-boilerplate/react-boilerplate/)
I waste 1 day to fix this problem, but no result. I was trying move fetch request to action (without saga) and result the same.
My component:
...
import { compose } from 'redux';
import injectReducer from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
import { successFetching } from './actions';
import reducer from './reducers';
import saga from './saga';
class NewsListPage extends React.PureComponent {
componentDidMount() {
const { dispatch } = this.props
dispatch(saga())
}
...
};
NewsListPage.propTypes = {
isFetching: PropTypes.bool.isRequired,
isSuccess: PropTypes.bool.isRequired,
items: PropTypes.array.isRequired,
dispatch: PropTypes.func.isRequired,
}
const selector = (state) => state;
const mapStateToProps = createSelector(
selector,
(isFetching, isSuccess, items) => ({ isFetching, isSuccess,items })
);
function mapDispatchToProps(dispatch) {
return {
dispatch,
};
}
const withReducer = injectReducer({ key: 'NewsList', reducer });
const withSaga = injectSaga({ key: 'NewsList', saga });
const withConnect = connect(mapStateToProps, mapDispatchToProps);
export default compose(
withReducer,
withSaga,
withConnect
)(NewsListPage);
My actions:
export const NEWS_FETCH_LOADING = 'NEWS_FETCH_LOADING';
export const NEWS_FETCH_FAILURE = 'NEWS_FETCH_FAILURE';
export const NEWS_FETCH_SUCCESS = 'NEWS_FETCH_SUCCESS';
export function preFetching() {
return {
type: NEWS_FETCH_LOADING,
}
}
export function successFetching(json) {
return {
type: NEWS_FETCH_SUCCESS,
payload: json
}
}
export function failureFetching(error) {
return {
type: NEWS_FETCH_FAILURE,
payload: error
}
}
My reducers:
...
import { NEWS_FETCH_LOADING, NEWS_FETCH_FAILURE, NEWS_FETCH_SUCCESS } from './actions'
const INITIAL_STATE = {
isFetching: false,
isSuccess: false,
items: []
};
function NewsListReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case NEWS_FETCH_LOADING:
case NEWS_FETCH_FAILURE:
return Object.assign({}, state, {
isFetching: true,
isSuccess: false
})
case NEWS_FETCH_SUCCESS:
return Object.assign({}, state, {
isFetching: false,
isSuccess: true,
items: action.payload,
})
default:
return Object.assign({}, state)
}
}
export const rootReducer = combineReducers({
NewsListReducer
})
export default rootReducer
My saga:
import { call, put, select, takeLatest } from 'redux-saga/effects';
import {NEWS_FETCH_LOADING, NEWS_FETCH_FAILURE, NEWS_FETCH_SUCCESS, preFetching, successFetching, failureFetching} from './actions';
import request from 'utils/request';
export function* getNews() {
const requestURL ='%MY_URL%';
try {
const req = yield call(request, requestURL);
yield put(successFetching(req));
} catch (err) {
yield put(failureFetching(err));
}
}
export default function* NewsListSaga() {
yield takeLatest(NEWS_FETCH_SUCCESS, getNews);
}
EDIT:
#Alleo Indong, i tried your advice and its almost work.
I change in component mapDispatchToProps to
export function mapDispatchToProps(dispatch) {
return {
getData: () => dispatch(loadNews()),
};
}
And add new function to actions.js
export function loadNews() {
return {
type: NEWS_FETCH_SUCCESS,
}
}
But now ajax sent every seconds like in while cycle. I tried call this.props.getData(); in componentDidMount and in constructorand result same.
EDIT 2
In component i add
import * as actionCreators from './actions';
import { bindActionCreators } from 'redux';
In constructor i change
constructor(props) {
super(props);
const {dispatch} = this.props;
this.boundActionCreators = bindActionCreators(actionCreators, dispatch)
}
But here dispatch is undefined and in componentDidMount too.
And change mapDispatchToProps to
export function mapDispatchToProps(dispatch) {
return {
...bindActionCreators(actionCreators, dispatch),
};
}
Hello #AND and welcome to stackoverflow! As I mentioned in the comment on the main post, you are dispatching a GENERATOR instead of an object on your
dispatch(saga());
Here's an example to help you
On your component import the actions that you want to use like this.
import * actionCreators from './actions';
import { bindActionCreators } from 'redux';
Learn more about bindActionCreators here enter link description here
This will import all of the exported actionCreators that you created there.
In my opinion you don't need successFetching and failureFetching anymore as you can dispatch this actions later on on your saga
Then in your mapDispatchToProps you would want to register this actioncreator
function mapDispatchToProps(dispatch) {
return {
...bindActionCreators(actionCreators, dispatch),
};
}
Then on your saga, I want to point up some problems here as well.
First your function
export default function* NewsListSaga() {
yield takeLatest(NEWS_FETCH_SUCCESS, getNews);
}
Is actually what we call a watcher where it watch when a certain action was dispatch, in this function you are already waiting for the NEWS_FETCH_SUCCESS before getNews is called which is wrong, because you first have do the FETCHING before you will know if it is failed or success so yeah this function should be like this
export default function* NewsListSaga() {
yield takeLatest(NEWS_FETCH_LOADING, getNews);
}
This simple means that you will take all the latestNEWS_FETCH_LOADINGactions that was dispatched and will call thegetNews`.
then on your getNews generator function, you can do it like this
export function* getNews() {
const requestURL ='%MY_URL%';
try {
const req = yield call(request, requestURL);
yield put({type: NEWS_FETCH_SUCCESS, req});
} catch (err) {
yield put({type: NEWS_FETCH_FAILED, err});
}
}
in here
const req = yield call(request, requestURL);
You are saying that you will wait for the result of the request / service that you called, it might be a promise.
Then in here, this is why you won't need the functions successFetching and failureFetching functions anymore, since you can do it like this
yield put({type: NEWS_FETCH_SUCCESS, req});
One last important step that you have to do now is to call the actionCreator inside your componentDidMount()
like this
componentDidMount() {
const { preFetching } = this.props;
preFetching();
}
I have a problem with my redux app: it correctly dispatches the relevant action and updates the state, but for some reason the UI doesn't get updated. 99% of questions about this seem to be caused because the state actually gets mutated, but I'm pretty sure that's not the case. Here are the relevant files:
Container component:
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import requestEvents from 'actions/feedActions';
import { FeedElement, feedElementTypes } from '../components/feed/feedElement';
class Feed extends React.Component {
componentWillMount() {
this.props.requestEvents();
}
render() {
console.log('Rendering feed');
const listOfFeedElements = this.props.listOfFeedElements;
let elementsToDisplay;
if (listOfFeedElements.length === 0) {
elementsToDisplay = <li><p> No elements to display</p></li>;
} else {
const numOfElementsToDisplay = listOfFeedElements.length <= 10 ?
listOfFeedElements.length : 10;
const sortedElementsToDisplay = listOfFeedElements.concat().sort(
(e1, e2) => e1.timestamp - e2.timeStamp);
elementsToDisplay =
sortedElementsToDisplay
.slice(0, numOfElementsToDisplay)
.map(el => FeedElement(el));
}
return (
<div className="socialFeedContainer">
<h1> Social feed </h1>
<ol>
{elementsToDisplay}
</ol>
</div>
);
}
}
Feed.propTypes = {
requestEvents: PropTypes.func.isRequired,
listOfFeedElements: PropTypes.arrayOf(PropTypes.shape({
timeStamp: PropTypes.number.isRequired,
type: PropTypes.oneOf(Object.keys(feedElementTypes)).isRequired,
targetDeck: PropTypes.string.isRequired,
concernedUser: PropTypes.string.isRequired,
viewed: PropTypes.bool.isRequired })),
};
Feed.defaultProps = {
listOfFeedElements: [],
};
function mapDispatchToProps(dispatch) {
return bindActionCreators({ requestEvents }, dispatch);
}
function mapStateToProps(state) {
const { feedState } = state.local.feed.listOfFeedElements;
return {
feedState,
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Feed);
Reducers:
import Immutable from 'seamless-immutable';
import { types } from 'actions/feedActions';
const initialState = Immutable({
listOfFeedElements: [],
sentRequestForList: false,
receivedList: false,
});
function feedReducer(state = initialState, action) {
switch (action.type) {
case types.REQUEST_FEED_ELEMENTS:
return Immutable.merge(state, {
sentRequestForList: true,
receivedList: false,
});
case types.RECEIVED_LIST:
return Immutable.merge(state, {
sentRequestForList: false,
receivedList: true,
listOfFeedElements: action.payload.listOfNotifications,
});
default:
return state;
}
}
export default feedReducer;
Saga:
import { takeLatest, put } from 'redux-saga/effects';
import { types } from 'actions/feedActions';
import feedApiCall from './feedApiCall';
export function* getFeedFlow() {
try {
console.log('sent request for feed');
const listOfNotifications = feedApiCall();
yield put({
type: types.RECEIVED_LIST,
payload: {
listOfNotifications,
},
});
} catch (error) {
yield put({
type: types.RECEPTION_ERROR,
payload: {
message: error.message,
statusCode: error.statusCode,
},
});
}
}
function* feedUpdateWatcher() {
yield takeLatest(types.REQUEST_FEED_ELEMENTS, getFeedFlow);
}
export default feedUpdateWatcher;
The actions get dispatched and the state modified (In the end I have a list with the components). However the component renders only once, as I can check from the call to console.log.
The root component looks quite suspiciously.
First, proceeding from a code and agreements on naming, FeedElement the class looks as React, however it is used as simple function in map. Whether there was no similar look in view of something:
elementsToDisplay =
sortedElementsToDisplay
.slice(0, numOfElementsToDisplay)
.map(el => (<FeedElement {...el} />));
Secondly, you return object const {feedState} = state.local.feed.listOfFeedElements; and further you address this.props.listOfFeedElements - such field in principle just doesn't exist. And from where a part of state.local.feed undertakes?
In addition, time you is used by redux and saga, it is better to make generally a pure functional part and to get rid of a call of componentWillMount which not really corresponds to the expected architecture.