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;
}
}
Related
I have a react app that is connected with redux. The component has a form that makes a PUT call to the api when the form is submitted. When I submit the form, I can see that redux gets updated accordingly but when I try to access the redux state as a prop in my component, the props data does not return the current data and is off by 1. For example, here's the data in my redux store:
Redux store:
When I do console.log("THIS PROPS: ", this.props) in my component, I see that it accountError is showing up as null
When I dispatch the action again the second time, only then I see that I am getting the data from redux in my props:
Here is the code that I have currently:
OrgAccount.js
import { registerOrgAccount, getListOfOrgsAndAccts } from "../../store/actions";
handleSubmit = () => {
this.props.registerOrgAccount(this.state)
console.log("THIS PROPS: ", this.props)
if(this.props.accountError === null) {
this.toggleTab(this.state.activeTab + 1);
}
};
<Link
to="#"
className="btn w-lg"
onClick={() => {
if (this.state.activeTab === 1) {
this.handleSubmit();
}
}}
>
Next
</Link>
const mapStatetoProps = (state) => {
const { accounts, accountError, loading } = state.OrgAccount;
return { accounts, accountError, loading };
};
const mapDispatchToProps = (dispatch) => {
return {
getListOfOrgsAndAccts: () => {
dispatch(getListOfOrgsAndAccts())
},
registerOrgAccount: (data) => {
dispatch(registerOrgAccount(data))
},
}
}
export default connect(mapStatetoProps, mapDispatchToProps)(OrgAccount);
Reducer:
const initialState = {
accountError: null, accountsError: null, message: null, loading: null
}
const orgAccount = (state = initialState, action) => {
switch (action.type) {
case REGISTER_ORG_ACCOUNT:
state = {
...state,
account: null,
loading: true,
// accountError: null
}
break;
case REGISTER_ORG_ACCOUNT_SUCCESSFUL:
state = {
...state,
account: action.payload,
loading: false,
accountError: null
}
break;
case REGISTER_ORG_ACCOUNT_FAILED:
state = {
...state,
loading: false,
accountError: action.payload ? action.payload.response : null
}
break;
...
default:
state = { ...state };
break;
}
return state;
}
export default orgAccount;
Action
export const registerOrgAccount = (account) => {
return {
type: REGISTER_ORG_ACCOUNT,
payload: { account }
}
}
export const registerOrgAccountSuccessful = (account) => {
return {
type: REGISTER_ORG_ACCOUNT_SUCCESSFUL,
payload: account
}
}
export const registerOrgAccountFailed = (error) => {
return {
type: REGISTER_ORG_ACCOUNT_FAILED,
payload: error
}
}
Saga.js
import { registerOrgAccountSuccessful, registerOrgAccountFailed, getListOfOrgsAndAcctsSuccessful, getListOfOrgsAndAcctsFailed } from './actions';
import { putOrgAccount } from '../../../helpers/auth_helper';
function* registerOrgAccount({ payload: { account } }) {
try {
const response = yield call(putOrgAccount, {
orgId: account.orgId,
accountNumber: account.accountNumber,
accountName: account.accountName,
accountCode: account.accountCode,
urlLink: account.urlLink,
location: account.location,
accountType: account.accountType,
address: account.address,
city: account.city,
state: account.state,
zip: account.zip,
country: account.country,
email: account.email,
eula: "blah"
});
yield put(registerOrgAccountSuccessful(response));
} catch (error) {
yield put(registerOrgAccountFailed(error));
}
}
To understand the root cause here, I think it helps to know a little about immutability and how React rerenders. In short, React will rerender when it detects reference changes. This is why mutating a prop, wont trigger a rerender.
With that in mind, at the time you call handleSubmit, this.props.accountError is simply a reference to a value somewhere in memory. When you dispatch your action and your state is updated, a new reference will be created, which will trigger a rerender of your component. However the handleSubmit function that was passed to your element still references the old this.props.accountError, which is why it is still null.
You could get around this by implementing your check in the componentDidUpdate lifecycle method. E.g. something like this:
componentDidUpdate(prevProps) {
if (prevProps.accountError === null && this.props.accountError !== null) {
this.toggleTab(this.state.activeTab + 1)
}
}
I am like in a strange problem. The problem is that I am trying to make an API hit (in service file) which in turn provides some data (it is working), this data is to be updated in my reducer1.js and then returned. Now, my issue is though the value is coming in reducer file, but is not returned, so in turn, state is not changed, and in turn my end component is not rerendered.
Now, when my service file is successfully hitting and then returning data to my reducer1.js, why in the world the updated-state is not returned by "GET_List" action type? Can someone see any problem?
index.js (service file)
const global = {
getActressList: async function(){
const response = await fetch("http://localhost:2000/api/actressList");
const data = await response.json();
return data;
}
}
export default global;
reducer1.js
import global from '../../services/index';
const initialState = {
data: [
{
id: 1,
name: "Aishwarya Rai",
src: "/assets/img/aishwarya.png"
}
]
};
function reducer1(state = initialState, action) {
switch (action.type) {
case "GET_LIST": {
const data = global.getActressList();
data.then((res)=> {
return {
...state,
data: res
}
})
}
default:
return state;
}
}
export default reducer1;
Result:
You are returning from a promise not from a reducer function:
function reducer1(state = initialState, action) {
switch (action.type) {
case "GET_LIST": {
const data = global.getActressList();
data.then((res) => {
// here you are returning from a promise not from a reducer function
return {
...state,
data: res,
};
});
}
default:
return state;
}
}
The code in reducer should be sync like this:
function reducer1(state = initialState, action) {
switch (action.type) {
case "GET_LIST": {
return {
...state,
data: action.payload,
};
}
default:
return state;
}
}
And your data fetching should be moved to component effect like this:
function YourComponent() {
const dispatch = useDispatch();
const data = useSelector(state => state.data)
useEffect(() => {
const data = global.getActressList();
data.then((res) => {
dispatch({type: 'GET_LIST', payload: res});
});
}, [])
...
}
EDIT
If you use class components the fetching logic should be placed in componentDidMount lifecycle hook like this:
class YourComponent extends Component {
state = { data: [] };
componentDidMount() {
const data = global.getActressList();
data.then((res) => {
dispatchYourAction({type: 'GET_LIST', payload: res});
});
}
...
}
If I fetch this array of restos with redux:
[{
res_id: Int,
res_name: String,
res_category: String,
res_category_id: Int,
city_id: Int
}]
My action looks something like this:
export const getrestos = () => {
const resData = await response.json();
dispatch({
type: GET_RESTOS,
payload: resData
});
};
};
export const setFilters = filterSettings => {
console.log(filterSettings);
return { type: SET_FILTERS, filters: filterSettings };
};
And this is my reducer:
import { GET_RESTOS, SET_FILTERS } from '../actions/restos';
const initialState = {
restoList: [],
filteredRestos: []
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_RESTOS:
return {
restoList: action.payload
}
case SET_FILTERS:
const appliedFilters = action.filters;
const updatedFilteredRestos = state.restoList.filter(resto => {
if (appliedFilters.cityID || resto.city_id) {
resto => resto.city_id.indexOf(cityID) >= 0
return { ...state, filteredRestos: updatedFilteredRestos };
}
});
return { ...state, filteredRestos: updatedFilteredRestos };
default:
return state;
}
};
I have touchable categorys in a page, and when i touch one i want to fetch the corresponding restos for that category and show them in a flatlist. Apart from that i want to have a search bar that when I type I want to show restos by res_name and/or by res_category.
Ive tried to create selectors, but I dont understand how, i dont need an specific approach, but the most clean or efficient as possible.
Thanks in advance if anyone can give me a hint or solution!
EDIT
The problem is im getting undefined in updatedFilteredRestos.
Your reducers should be clean, dumb and all they do should be returning objects. This makes your components more testable and errors easier to catch. In my opinion, this is a perfect use-case for reselect. Here's a medium article: https://medium.com/#parkerdan/react-reselect-and-redux-b34017f8194c But the true beauty of reselect is that it will memoize for you, i.e. if your states don't change, it uses a cached version of the data.
Anyway, you should clean up your restoReducer to something to this effect.
import { GET_RESTOS, SET_FILTERS } = "../actions/restos";
const initialState = {
restoList: [],
filteredRestos: []
};
const restoReducer = (state = initialState, action) => {
switch(action.type) {
case GET_RESTOS:
return { ...state, restoList: action.payload };
case SET_FILTERS:
return { ...state, filteredRestos: action.payload };
default:
return state;
}
}
Then write your filtered resto selector:
// ../selectors/restos
import { createSelector } from "reselect";
// First, get your redux states
const getRestos = (state) => state.restos.restoList;
const getFilteredRestos = (state) => state.restos.filteredRestos;
// Next, create selectors
export const getFilteredRestoList = createSelector(
[getRestos, getFilteredRestos],
(restoList, filteredRestos) => {
// need to check for non-empty filters
// if it is, simply return the unfiltered `restoList`
if(!Array.isArray(filteredRestos) || !filteredRestos.length)
return restoList || [];
// If you do have valid filters, return filtered logic
return restoList.filter(r => filteredRestos.some(f => f.cityID === r.city_id));
);
Then, use this selector in your components:
// ../components/my-app
import { getFilteredRestoList } from "../selectors/restos";
// hook it up to your `mapStateToProps` as you would a normal state
// except this time, it's a special selector
const mapStateToProps = (state, ownProps) => {
restoList: state.restos.restoList,
filteredRestos: state.restos.filteredRestos,
filteredRestoList: getFilteredRestoList(state) //<-- this is your selector
}
Then inside your component, just reference it: this.props.filteredRestoList.
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 do not know how to access a boolean isLoading flag from reducerForm.js reducer in reducerRegister.js. I have used combineReducers() and I use isLoading to disable a button during form submit.
It's initial state is false, after clicking submit, it changes to true. After the form submission is successful, isLoading is reset to false again. Below is the relevant code for this issue:
actionRegister.js
let _registerUserFailure = (payload) => {
return {
type: types.SAVE_USER_FAILURE,
payload
};
};
let _registerUserSuccess = (payload) => {
return {
type: types.SAVE_USER_SUCCESS,
payload,
is_Active: 0,
isLoading:true
};
};
let _hideNotification = (payload) => {
return {
type: types.HIDE_NOTIFICATION,
payload: ''
};
};
// asynchronous helpers
export function registerUser({ // use redux-thunk for asynchronous dispatch
timezone,
password,
passwordConfirmation,
email,
name
}) {
return dispatch => {
axios.all([axios.post('/auth/signup', {
timezone,
password,
passwordConfirmation,
email,
name,
is_Active: 0
})
// axios.post('/send', {email})
])
.then(axios.spread(res => {
dispatch(_registerUserSuccess(res.data.message));
dispatch(formReset());
setTimeout(() => {
dispatch(_hideNotification(res.data.message));
}, 10000);
}))
.catch(res => {
// BE validation and passport error message
dispatch(_registerUserFailure(res.data.message));
setTimeout(() => {
dispatch(_hideNotification(res.data.message));
}, 10000);
});
};
}
actionForm.js
export function formUpdate(name, value) {
return {
type: types.FORM_UPDATE_VALUE,
name, //shorthand from name:name introduced in ES2016
value
};
}
export function formReset() {
return {
type: types.FORM_RESET
};
}
reducerRegister.js
const INITIAL_STATE = {
error:{},
is_Active:false,
isLoading:false
};
const reducerSignup = (state = INITIAL_STATE , action) => {
switch(action.type) {
case types.SAVE_USER_SUCCESS:
return { ...state, is_Active:false, isLoading: true, error: { register: action.payload }};
case types.SAVE_USER_FAILURE:
return { ...state, error: { register: action.payload }};
case types.HIDE_NOTIFICATION:
return { ...state , error:{} };
}
return state;
};
export default reducerSignup;
reducerForm.js
const INITIAL_STATE = {
values: {}
};
const reducerUpdate = (state = INITIAL_STATE, action) => {
switch (action.type) {
case types.FORM_UPDATE_VALUE:
return Object.assign({}, state, {
values: Object.assign({}, state.values, {
[action.name]: action.value,
})
});
case types.FORM_RESET:
return INITIAL_STATE;
// here I need isLoading value from reducerRegister.js
}
return state;
};
export default reducerUpdate;
reducerCombined.js
import { combineReducers } from 'redux';
import reducerRegister from './reducerRegister';
import reducerLogin from './reducerLogin';
import reducerForm from './reducerForm';
const rootReducer = combineReducers({
signup:reducerRegister,
signin: reducerLogin,
form: reducerForm
});
export default rootReducer;
This is where I use isLoading:
let isLoading = this.props.isLoading;
<FormGroup>
<Col smOffset={4} sm={8}>
<Button type="submit" disabled={isLoading}
onClick={!isLoading ? isLoading : null}
>
{ isLoading ? 'Creating...' : 'Create New Account'}
</Button>
</Col>
</FormGroup>
Mapping state to props within the same component
function mapStateToProps(state) {
return {
errorMessage: state.signup.error,
isLoading: state.signup.isLoading,
values: state.form.values
};
}
This is covered in the Redux FAQ at https://redux.js.org/faq/reducers#how-do-i-share-state-between-two-reducers-do-i-have-to-use-combinereducers:
Many users later want to try to share data between two reducers, but find that combineReducers does not allow them to do so. There are several approaches that can be used:
If a reducer needs to know data from another slice of state, the state tree shape may need to be reorganized so that a single reducer is handling more of the data.
You may need to write some custom functions for handling some of these actions. This may require replacing combineReducers with your own top-level reducer function. You can also use a utility such as reduce-reducers to run combineReducers to handle most actions, but also run a more specialized reducer for specific actions that cross state slices.
Async action creators such as redux-thunk have access to the entire state through getState(). An action creator can retrieve additional data from the state and put it in an action, so that each reducer has enough information to update its own state slice.
A reducer cannot access another reducer's state, but if you're using redux-thunk you can do so from within an action creator. As an example, you can define an action creator like this:
export const someAction = () =>
(dispatch, getState) => {
const someVal = getState().someReducer.someVal;
dispatch({ type: types.SOME_ACTION, valFromOtherReducer: someVal });
};
React Redux works on unidirectional data flow.
Action ---> Reducer /store ---> Reducer
Reducer works on small subset of store, you can not access store inside reducer which is not part of Reducer. you can either need to fire new action from the component based on reducer state return.