This occurs with React + Redux Saga application my team is working on
We buy the React Isomorphic theme from Themeforest which bundles the Redux, Saga, and React Router V.4. We are now working on top of it.
I have been using React for a while but new to Redux and never experienced with such behavior. The problem is that whenever I dispatch an action, the component unmounts and remounts as the state changes
Okay, what I am doing is to fetch some user data from the API but. So this is how I come up with the following action & reducer
// Actions.js
const UserActions = {
FETCH_USER_REQUEST: 'FETCH_USER_REQUEST',
FETCH_USER_SUCCESS: 'FETCH_USER_SUCCESS',
fetch_users: () => {
return {
type: UserActions.FETCH_USER_REQUEST
}
}
}
export default UserActions;
And the reducer
// Reducer.js
export default function UserReducer(state = initialState, action) {
switch (action.type) {
case 'REQUEST_USER_SUCCESS':
return state.set('user_list', action.user_list);
default:
return state;
}
}
With Redux Saga, the middleware is created to handle async actions. This is how it looks
// Saga.js
import { all, takeEvery, call, put, fork } from 'redux-saga/effects';
import {get, post} from 'axios';
export function fetchUser() {
return get('https://mockapi.testapp.dev/users')
.then(response => {
return response.data;
}).catch(error => {
return error.data
});
}
export function* fetchUserRequest() {
yield takeEvery('FETCH_USER_REQUEST', function*() {
const resp = yield call(fetchUser);
yield put({
action: 'FETCH_USER_SUCCESS',
user_list: resp
});
});
}
export default function* rootSaga() {
yield all([
fork(fetchUserRequest)
]);
}
Now I implement the code in my component like this
// App.js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import UserActions from './actions/UserAction';
const mapStateToProps = (state, ownProps) => {
return {
userList: state.User.get('user_list')
}
}
const mapDispatchToProps = dispatch => {
return {
fetchUser: () => dispatch(UserActions.fetch_users())
}
}
class App extends Component {
componentDidMount() {
this.props.fetchUser();
}
render() {
// ... The rest of the rendering processes
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
Now that you can see, with the behavior mentioned prior to this. Dispatching an action via this.props.fetchUser() cause the state to change but what I don't expect is that the component shouldn't unmount and remount because once it does so, an infinite loop occurs because the componentDidMount runs over and over and state also changes accordingly.
What I expect is to fetch the data from the API once the component mounts without remounting itself once the state changes for any reason because the theme we purchased is equipped with other base components which make use of Redux-saga to handle state and async actions. For example, the collapsable sidebar triggers a dispatch which changes the state that controls its bahavior once the users click on the it. Currently, once it does that my current component immediately unmounts unexpectedly.
Is there any possible way to solve such a problem or this is the default behavior of Redux?
Related
so I am trying to refactor some code from my previous question:
React: How to update one component, when something happens on another component
So I started digging deep into the existing code template to see how it was implemented.
I found a reducers.js where I added a new reducer: ActiveTenant
import Auth from './auth/reducer';
import App from './app/reducer';
import ThemeSwitcher from './themeSwitcher/reducer';
import LanguageSwitcher from './languageSwitcher/reducer';
import ActiveTenant from './activetenant/reducer';
export default {
Auth,
App,
LanguageSwitcher,
ThemeSwitcher,
ActiveTenant
};
That new reducer is like this:
import { Map } from 'immutable';
import actions from './actions';
import { adalApiFetch } from '../../adalConfig';
const initState = new Map({
tenantId: ''
});
export default function(state = initState, action) {
switch (action.type) {
case actions.SET_TENANT_ACTIVE:
{
const options = {
method: 'post'
};
adalApiFetch(fetch, "/Tenant/SetTenantActive?TenantName="+state.tenantId, options)
.then(response =>{
if(response.status === 200){
console.log("Tenant activated");
}else{
throw "error";
}
})
.catch(error => {
console.error(error);
});
return state.set('tenant', state.Name);
}
default:
return state;
}
}
and actions for that reducer
const actions = {
SET_TENANT_ACTIVE: 'SET_TENANT_ACTIVE',
setTenantActive: () => ({
type: actions.SET_TENANT_ACTIVE
}),
};
export default actions;
Then from the component itself, I need to call the action when a row is selected on the front end, so I have refactored the commented code, into one line.
import React, { Component } from 'react';
import { Table, Radio} from 'antd';
import { adalApiFetch } from '../../adalConfig';
import Notification from '../../components/notification';
import actions from '../../redux/activetenant/actions';
const { setTenantActive } = actions;
class ListTenants extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
fetchData = () => {
adalApiFetch(fetch, "/Tenant", {})
.then(response => response.json())
.then(responseJson => {
if (!this.isCancelled) {
const results= responseJson.map(row => ({
key: row.id,
TestSiteCollectionUrl: row.TestSiteCollectionUrl,
TenantName: row.TenantName,
Email: row.Email
}))
this.setState({ data: results });
}
})
.catch(error => {
console.error(error);
});
};
componentDidMount(){
this.fetchData();
}
render() {
const columns = [
{
title: 'TenantName',
dataIndex: 'TenantName',
key: 'TenantName',
},
{
title: 'TestSiteCollectionUrl',
dataIndex: 'TestSiteCollectionUrl',
key: 'TestSiteCollectionUrl',
},
{
title: 'Email',
dataIndex: 'Email',
key: 'Email',
}
];
// rowSelection object indicates the need for row selection
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
if(selectedRows[0].TenantName != undefined){
console.log(selectedRows[0].TenantName);
const options = {
method: 'post'
};
setTenantActive(selectedRows[0].TenantName);
/* adalApiFetch(fetch, "/Tenant/SetTenantActive?TenantName="+selectedRows[0].TenantName.toString(), options)
.then(response =>{
if(response.status === 200){
Notification(
'success',
'Tenant set to active',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Tenant not activated',
error
);
console.error(error);
}); */
}
},
getCheckboxProps: record => ({
type: Radio
}),
};
return (
<Table rowSelection={rowSelection} columns={columns} dataSource={this.state.data} />
);
}
}
export default ListTenants;
However, its not clear to me the relationship between the action and the reducer, if I check the debugger the action is executed, and none parameter is received, but the reducer is never executed.
DO i have to put a dispatch somewhere?, what I am missing in this puzzle?
So the first thing to understand is the Redux Cycle:
Action Creator-->Action-->dispatch-->Reducers-->State
Action Creator: An action creator is a function that is going to create or return a plain JavaScript object knowns as an Action with a type property and payload property which describes some change you want to make on your data.
The payload property describes some context around the change we want to make.
The purpose of an Action is to describe some change to the data inside our application.
The Action Creator is to create the Action.
The dispatch function is going to take in an Action and make copies of that object and pass it off to a bunch of different places inside our application which leads us to the Reducers.
In Redux, a reducer is a function responsible for taking in an Action. Its going to process that Action, make some change to the data and return it so it can be centralized in some location.
In Redux, the State property is a central repository of all information produced by our reducers. All the information gets consolidated inside the State object so our React application can easily reach into our Redux side of the app and get access to all the data inside the application.
So this way the app does not have to go around to each separate reducer and ask for the current State.
So digest that for a couple of minutes and then look at your architecture.
Let's skip over to reducers.
Reducers are called with an Action that was created by an Action Creator. The reducer will take a look at that Action and decide whether it needs to modify some data based on that Action.
So in other words, the job of a reducer is not to execute API requests but to process actions sent to it by the action creator.
So instead of this:
import { Map } from 'immutable';
import actions from './actions';
import { adalApiFetch } from '../../adalConfig';
const initState = new Map({
tenantId: ''
});
export default function(state = initState, action) {
switch (action.type) {
case actions.SET_TENANT_ACTIVE:
{
const options = {
method: 'post'
};
adalApiFetch(fetch, "/Tenant/SetTenantActive?TenantName="+state.tenantId, options)
.then(response =>{
if(response.status === 200){
console.log("Tenant activated");
}else{
throw "error";
}
})
.catch(error => {
console.error(error);
});
return state.set('tenant', state.Name);
}
default:
return state;
}
}
Your reducer should look something like this:
import { SET_TENANT_ACTIVE } from "../actions/types";
const initialState = {
tenantId: ''
};
export default (state = initialState, action) {
switch (action.type) {
case SET_TENANT_ACTIVE:
return {...state, [action.payload.id]: action.payload };
default:
return state;
}
}
Then inside your action creators file, you should have an action creator that looks something like this:
import axios from 'axios';
import { SET_TENANT_ACTIVE } from "../actions/types";
export const setTenant = id => async (dispatch) => {
const response = await axios.post(`/tenants/${id}`);
dispatch({ type: SET_TENANT_ACTIVE, payload: response.data });
};
You also need to learn about Redux project structure because after the above refactor, you are missing how to wire all this up to your component. In your component file there is no connect() function which also requires the Provider tag and you have none of that.
So for this I recommend first of all your set up your folder and file structure like so:
/src
/actions
/components
/reducers
index.js
So inside your index.js file it should look something like this:
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware, compose } from "redux";
import reduxThunk from "redux-thunk";
import App from "./components/App";
import reducers from "./reducers";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducers,
composeEnhancers(applyMiddleware(reduxThunk))
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
So your goal here is to ensure that you get that Provider tag at the very top of your component hierarchy and ensure that you pass it a reference to your Redux store that gets all the reducers loaded up into it.
So above I have created the store and passed it our set of reducers and it will return back to you all your applications State.
Lastly, what you see above is I created an instance of <Provider> and wrapped the <App /> component with it and then you want to pass the <Provider> component is a single prop called store. The store is the result of calling createStore() and calling the reducers.
The <Provider> is what interacts with the Redux store on our behalf.
Notice, I also have wired up Redux-Thunk that J. Hesters mentioned, you are making an ajax request as far as I can see from your code which is why I offered an asynchronous action creator for you, which means you will need Redux-Thunk or some middleware like that, let me not offend the Redux-Saga fans, so you have those two choice at least. You seem relatively new to Redux, just go with Redux-Thunk.
Now you can use the connect() component inside your component file to finish wiring up those action creators and reducers to your component or your React side of the application.
import React, { Component } from 'react';
import { connect } from "react-redux";
import { Table, Radio} from 'antd';
import { adalApiFetch } from '../../adalConfig';
import Notification from '../../components/notification';
import actions from '../../redux/activetenant/actions';
After importing connect, you create an instance of it below:
export default connect()(ListTenants);
Please don't argue with me on the above syntax (actually had a former student report me to administrators for using this syntax as evidence of not knowing what I was doing).
Then you need to configure this connect() React component by adding mapStateToProps if you are going to need it, but definitely pass in actions as the second argument to connect(). If you realize you don't need mapStateToProps, then just pass in null as the first argument, but you can't leave it empty.
Hope all this was helpful and welcome to the wonderful world of React-Redux.
You are using reducers wrong. Reducers are supposed to be pure. Yours has side-effects showing that you haven't understood Redux, yet.
Instead of writing down a solution for you (which would take forever anyways since one would have to explain Redux in total), I suggest you invest the 3 hours and go through the Redux docs and follow the tutorials (they are great).
Afterwards you might want to look into Redux Thunk. But, you might not need thunks.
PS: (Small thing to bring up, but I haven't seen anyone using Maps in Redux. Is there a reason you do that? You might want to use plain objects instead.)
Your action is not correct you should pass an active tenant name as parameter.
Ref. https://redux-starter-kit.js.org/api/createaction
We could have written the action types as inline strings in both places.
The action creators are good, but they're not required to use Redux - a component could skip supplying a mapDispatch argument to connect, and just call this.props.dispatch({type : "CREATE_POST", payload : {id : 123, title : "Hello World"}}) itself.
Ref. https://redux-starter-kit.js.org/usage/usage-guide
I just started to work on React Js and Redux-Thunk. Currently, I am trying to fetch data from a url using redux-thunk. I got data successfully but the issue is that it renders undefined data twice, then it gives me the desired data in props.
Here is my code.
In Actions
index.js
function getData() {
return {
type: 'FETCH'
}
}
function getSuccess(data) {
return {
type: 'FETCH_SUCCESS',
payload: data
}
}
function getFailed(err) {
return {
type: 'FAILED',
payload: err
}
}
export function fetchData() {
const thunk = async function thunk(dispatch) {
try {
dispatch(getData());
const body = await fetch("http://jsonplaceholder.typicode.com/users")
const res = await body.json();
console.log("Thunk", res);
dispatch(getSuccess(res));
}
catch(err) {
dispatch(getFailed(err));
}
}
return thunk;
}
In Reducers fetch.js
const initialState = {
state: []
}
export default function(state=initialState , actions) {
switch(actions.type) {
case 'FETCH':
return {
...state
}
case 'FETCH_SUCCESS':
return {
...state,
data: actions.payload
}
case 'FAILED':
return {
...state
}
default:
return state;
}
}
Reducers Index.js
import fetch from './fetch';
import { combineReducers } from 'redux';
const rootReducer = combineReducers ({
fetch
});
export default rootReducer;
App.js
import React, { Component } from 'react';
import './App.css';
import Main from './component/Main';
import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
console.log("STore", store.getState());
class App extends Component {
render() {
return (
<Provider store={store}>
<Main/>
</Provider>
);
}
}
export default App;
Main.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../actions';
class Main extends Component {
componentWillMount() {
const { fetchData } = this.props
fetchData();
}
render() {
let mydata = this.props.data.data;
console.log("Data .....<>", mydata);
return(
<div>
Main
</div>
);
}
}
const mapStateToProps =(state)=> {
return {
data: state.fetch
}
}
export default connect(mapStateToProps, {fetchData: actions.fetchData})(Main);
Output Screen Shot
Please let me know what i am doing wrong. Any help will be appreciated.
Thanks
This behavior is correct. You're doing everything in the normal way, except calling async operations in componentWillMount method instead of componentDidMount.
Read more here about it.
You need to know, that when you are using componentDidMount - you are handle this in a safe way due to commit phase in component lifecycle and it means that your request will be guaranteed trigger once instead of possible several times, which can be in render phase.
See here the visual diagram to understand this more.
Regarding several renderings - it can be explained easily.
First time, when your component is mounting you are calling asynchronous operation and it needs more time to load data than for component mounting. Thats why you are accessing data property of your initialState (which is empty array), and getting undefined.
When you response is ready and actions is being dispatched, redux trigger re-render of connected components with new state.
If you want to make your async-await syntax works you should make lifecycle with async keyword as well and await your request inside.
NOTE: It's safe, but in several cases it might cause unexpected bugs, so keep in mind. Nevertheless, I don't recommend to use it in a such way. Read about this topic in the another thread at SO.
I advice you to create some isLoading boolean status in your reducer and keep track whether data is loaded or not.
Good luck! Hope it will helps!
There is nothing wrong with your code, but there is one unnecessary action.
Why do you see two times undefined and then you see your data?
First one is coming from the initial render. You are making an async dispatching in your componentWillMount so render does not wait for it, then try to log your data. At that time it is undefined.
Your fetchData dispatches two actions: getData and getSuccess. You see second undefined because of getData action since it returns current state which props.data.data undefined at that time again.
You see the data since getSuccess updates your state now.
If you want to test this comment out getData in your fetchData function, just use getSuccess and change your console.log in the render like that:
mydata && console.log("Data .....<>", mydata);
I think getData action is unnecessary here, it does not do anything beside returning the current state. Also, try to use componentDidMount instead of componentWillMount since it will be deprecated in version 17.
I have a simple redux action and reducer that changes state without making any sort of web service request:
// Action
export const setMyState = (value) => {
return {
type: 'SET_STATE',
payload: value
};
};
// Reducer
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'SET_STATE':
return { myState: action.payload }
}
}
When I set the state via the action in my react component and immediately call that state from the redux prop binding, I do not get the current state:
import React from 'react';
import { connect } from 'react-redux';
import { setMyState } from '../actions';
class MyScreen extends React.Component {
componentWillMount() {
this.props.setMyState('value');
console.log(this.props.myState);
}
}
const mapStateToProps = state => {
const { myState } = state;
return myState;
};
export default connect(mapStateToProps, { setMyState })(MyScreen);
I understand that render will be called again when the redux state and binded props change, but what I'm trying to do is fire off another Redux action based on the resulting state. My options appear to be:
Figure out how to latest Redux state in React immediately after state change
Fire off a redux action in the reducer instead, which sounds like an anti-pattern.
Set some sort of state in my component or redux that the action should be fired on the next render, which seems clunky.
config your reducer like:
switch (action.type) {
case 'SET_STATE':
return {
...state,
loading: true,
};
case 'SET_STATE_SUCCESS':
return {
...state,
loading: false,
payload: value
};
}
and then in your component listen to
this.props.myState.loading
then trigger your action if (loading) and so on!
My solution is to use event loop handles the update, not the call stack. In particular for your case you the implementation would include setTimeout(() => {}); as the following:
import React from 'react';
import { connect } from 'react-redux';
import { setMyState } from '../actions';
class MyScreen extends React.Component {
componentWillMount() {
setTimeout(() => { this.props.setMyState('value'); },0);
setTimeout(() => { console.log(this.props.myState); },0);
}
}
const mapStateToProps = state => {
const { myState } = state;
return myState;
};
export default connect(mapStateToProps, { setMyState })(MyScreen);
What are you using this Redux state for?
If you are trying to access the data you had just set in the Redux state or if it's data that's already sitting in the Redux state you could dispatch both calls at the same time.
If you need to make the call because there is some work done to the data while setting it in the Redux state (I recommend doing this work in the action not the reducer) then componentWillReceiveProps(nextProps) will do what you need. You can compare the current props with the next props and trigger another update to the Redux state.
The best way to do this would probably be to use a Redux middleware.
I am having bit of a wrinkle with Redux-Saga as the reducers I had done isn't updating the store as it should suppose to do.
I have used Saga to load a static in-app data and then fired the action with the payload passing the data to reducers, I did console.log() to make sure the reducers are getting the payload from action, which they are - but the problem appears to happen when I return the data into the state so that it could be accessible within the components. In props I only get default state from the reducers, any help on this matter would be highly appreciated. Following is the code I am using;
actions.js
export function loadAppAction() {
return {
type: types.LOAD_APP
}
}
export function loadAppDataAction(data) {
return {
type: types.LOAD_APP_DATA,
payload: data
}
}
api.js
import appData from '../components/appData';
export function appDataResponse() {
return appData;
}
app.js
export class App extends Component {
constructor(props) {
super(props);
const { loadAppAction } = this.props;
loadAppAction();
}
render() {
const {
initialLoadData,
activateModalAction,
deactivateModalAction,
toggleModal
} = this.props;
console.log('props', this.props)
return (
<div className="app">
{
toggleModal &&
<SignInModal
deactivateModalAction={deactivateModalAction}
/>
}
</div>
}
}
function mapStateToProps({ initialLoadReducer, toggleModalReducer }) {
console.log('lets see', initialLoadReducer);
return {
initialLoadData: initialLoadReducer,
toggleModal: toggleModalReducer
};
};
export default connect(
mapStateToProps, {
loadAppAction: actions.loadAppAction,
activateModalAction: actions.activateModalAction,
deactivateModalAction: actions.deactivateModalAction,
})
(App);
initialLoadReducers.js
export default function (state = [], action) {
switch (action.type) {
case types.LOAD_APP_DATA:
return [...action.payload];
default:
return state;
}
}
saga - index.js
function* watchLoadAppAction() {
yield takeEvery(types.LOAD_APP, loadAppSaga);
}
export default function* rootSaga() {
yield all ([watchLoadAppAction()]);
}
loadAppSaga.js
export default function* loadAppSaga(action) {
const response = yield call(api.appDataResponse);
yield put(actions.loadAppDataAction(response));
}
Following is the screenshot of my console for reference
I would suggest to call loadAppAction when componentDidMount not in the constructor. React doc also suggested the same.
https://reactjs.org/docs/react-component.html#componentdidmount
export class App extends Component {
componentDidMount() {
this.props.loadAppAction();
}
...
}
Right so, I got down to the problem here, I was trying to initialise the app with an empty array which will not going to work anyway, as the component is expecting to receive props from redux which is an empty array. Which is why, React didn't create the DOM at its first run and that caused the app to stop re-rendering even though the props are changing.
To make it work, I now initialise the app with the same data structure but with empty string values and in the next step making the data available through Redux Saga into the reducer and passing it back into the React component.
I'm using Axios to make an AJAX call and the data returns undefined and then it consoles the array after a few seconds. I've tried componentDidMount and componentWillMount. I've tried making a constructor with initial state as the props. getInitial state is deprecated unless using React.createClass.
Here's my code, anything helps!
actions/index.js
import axios from 'axios';
import { FETCH_STRAINS } from './types';
const ROOT_URL = `https://www.cannabisreports.com/api/v1.0/strains?sort=name&page=3`;
export function fetchStrains() {
return dispatch => {
axios.get(ROOT_URL)
.then(response => {
dispatch({
type: FETCH_STRAINS,
payload: response.data.data
})
})
.catch( error => console.log(error));
}
}
reducer/index.js
import { FETCH_STRAINS } from '../actions/types';
import initialState from './initialState';
export default function(state = initialState.strains, action) {
switch(action.type) {
case FETCH_STRAINS:
return { ...state, strains: action.payload };
default:
return state;
}
}
app.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from './actions';
import './App.css';
class App extends Component {
componentWillMount() {
this.props.fetchStrains();
}
render() {
return (
<div className="App">
{this.props.strains === undefined ? console.log("this is undefined") : console.log(this.props.strains)}
</div>
);
}
}
function mapStateToProps( state ) {
return { strains: state.strains.strains }
}
export default connect(mapStateToProps, actions)(App);
The issue you're facing isn't because your code is wrong. From a quick glance it looks like you're doing it right.
The problem is that your app exists and is showing before you have all the data ready. The axios call takes a very long time to complete. Until it's done, your app is showing something to the user, whether you like it or not.
So between startup and data arrival, strains is going to be undefined. You'll have to decide what to show the user while they're waiting. A common solution is a spinner.
You need to use async actions & need to import thunk-middleware while you combine your reducers.
export function fetchStrains() {
// Thunk middleware knows how to handle functions.
// It passes the dispatch method as an argument to the function,
// thus making it able to dispatch actions itself.
return function (dispatch) {
// First dispatch: the app state is updated to inform
// that the API call is starting.
// The function called by the thunk middleware can return a value,
// that is passed on as the return value of the dispatch method.
// In this case, we return a promise to wait for.
// This is not required by thunk middleware, but it is convenient for us.
axios.get(ROOT_URL)
.then(response => {
dispatch({
type: FETCH_STRAINS,
payload: response.data.data
})
})
.catch( error => console.log(error));
}
}