Getting and setting values from Redux Store with Redux Saga - reactjs

I am trying to return a token without an API but it keeps returning undefined. The purpose at the moment is just to optionally show two elements dependent on whether or not there is a token in the props, preparing it for later when I actually implement the API.
I have tried different saga effects and amended my code more times than I can count. There does not seem to be a lot of information regarding setting initial state so I was wondering if that may be the issue and I am trying to select state that does not exist? Though I believe this should be handled by the reducer?
My code is as follows:
redux/store.js:
import { connectRouter, routerMiddleware } from 'connected-react-router';
import { createBrowserHistory } from 'history';
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import createSagaMiddleware from 'redux-saga';
import reducers from './reducers';
import rootSaga from './sagas';
const history = createBrowserHistory();
const routeMiddleware = routerMiddleware(history);
const sagaMiddleware = createSagaMiddleware();
const middlewares = [thunk, sagaMiddleware, routeMiddleware];
const composeEnhancers =
typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
})
: compose;
const store = createStore(
combineReducers({
...reducers,
router: connectRouter(history),
}),
composeEnhancers(applyMiddleware(...middlewares))
);
sagaMiddleware.run(rootSaga);
export { store, history };
redux/reducers.js:
import Auth from './auth/reducer';
export default {
Auth
};
redux/saga.js:
import { all } from 'redux-saga/effects';
import authSaga from './auth/saga';
export default function* rootSaga(getState) {
yield all([
authSaga()
]);
};
redux/auth/actions.js:
export const checkAuthAction = (value) => ({
type: 'CHECK_AUTH',
value
});
redux/auth/reducer.js:
import { checkAuthAction } from "./actions";
const initialState = {
token: true
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'CHECK_AUTH':
return {
...state,
token: action.token
};
default:
return state;
};
};
export default reducer;
redux/auth/saga.js
import { select, take } from 'redux-saga/effects';
import { checkAuthAction } from './actions';
// Selectors
const getToken = (state) => state.token;
function* authSaga() {
const token = yield select(getToken);
console.log(token);
};
export default authSaga;
Edit: forgot to include the component itself.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
class Nav extends Component {
componentDidMount() {
console.log(this.props);
}
render() {
return (
<nav className="nav">
{this.props.token ? (
<Col type="flex" align="center" className="nav__list__item" md={6}>
<Link to="/logout"><IntlMessages id="nav.logout.link" /></Link>
</Col>
) : (
<Col type="flex" align="center" className="nav__list__item" md={6}>
<Link to="/login"><IntlMessages id="nav.login.link" /></Link>
</Col>
)}
</nav>
);
};
}
const mapStateToProps = state => ({
token: state.token
});
const mapDispatchToProps = {
CHECK_AUTH: 'CHECK_AUTH'
};
export default connect(mapStateToProps, mapDispatchToProps)(Nav);

In order to get the token in the component you should change the mapStateToProps function:
const mapStateToProps = state => ({
token: state.Auth.token
});
In order to get it in the saga you should change the getToken selector
const getToken = (state) => state.Auth.token;

Related

useEffect does not run when the dependencies are changed

I am using React Redux for state management. I have an anonymous user and an admin. I have current user which is empty {} and I am changing it to store the user details when the admin logs in. So, in my application I want to show the admin navbar if there is a logged user and I would like to show the anonymous navbar when there is not. Currently, when the admin logs in, the navbar is changed, but when I click logout, navbar is not changed. How can I solve this issue?
This is my NavBar.js:
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import NavBarAdmin from "./NavBarAdmin";
import NavBarAnonymous from "./NavBarAnonymous";
function NavBar() {
const currentUser = useSelector((state) => state.currentUser);
const [currentView, setCurrentView] = useState('anonymous');
useEffect(() => {
console.log(currentUser);
if(currentUser==null || currentUser.length==0 || currentUser == undefined){
setCurrentView('anonymous');
}else{
setCurrentView('admin');
}
}, [currentUser])
const handleView = () => {
switch(currentView){
case "anonymous": return <>
<NavBarAnonymous/>
</>
case "admin":
return <>
<NavBarAdmin/>
</>
}
}
return(
<>
{handleView()}
</>
)
}
export default NavBar;
My actions.js looks like:
import actionTypes from "./actionTypes";
export function setCurrentUser(value) {
return{
type: actionTypes.SET_CURRENT_USER,
payload: value
}
}
actionTypes.js
const actionTypes = {
SET_CURRENT_USER: 'SET_CURRENT_USER',
}
export default actionTypes;
configureStore.js
import { applyMiddleware, compose, createStore } from "redux";
import thunkMiddleware from "redux-thunk";
import rootReducer from './reducers';
export default function configureStore(preloadedState) {
const middlewares = [thunkMiddleware];
const middlewareEnhancer = applyMiddleware(...middlewares);
const enhancers = [middlewareEnhancer];
const composedEnhancers = compose(...enhancers);
const store = createStore(rootReducer, preloadedState, composedEnhancers);
return store;
}
reducers.js:
import { combineReducers } from "#reduxjs/toolkit";
import actionTypes from "./actionTypes";
export function currentUser(state = {}, action) {
switch (action.type) {
case actionTypes.SET_CURRENT_USER:
return Object.assign({}, state, {
q: action.payload,
})
default:
return state
}
}
export default combineReducers({
currentUser
});
In the logout functionality I am using:
dispatch(setCurrentUser({}));
And when I click logout button, {} is printed in the console, which means that the current user is set to {}, but the NavBar is not re-rendered.

React, Redux, NextJS Weird state race overrides

I have an app with 2 reducers (city and doctor)
I use sagas.
I have a problem that with weird state overrides.
For example here is the start of fetch doctors action. As you see state was changed for doctor isLoading: false to isLoading: true
After that fetch cities action was started right before. And isLoading for doctors was changed back to false.
On every action dispatch, another reducer state reset.
I have doubt that it's a NextJS specific problem so root store is creating couple of times and cause a race condition.
Technologies: react, redux, react-redux, next-redux-wrapper, next-redux-saga, redux-saga
app.js
....
export default withRedux(createStore)(withReduxSaga(MyApp))
store.js
import { applyMiddleware, createStore } from 'redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer from './rootReducer'
import rootSaga from './rootSaga'
const bindMiddleware = middleware => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension')
return composeWithDevTools(applyMiddleware(...middleware))
}
return applyMiddleware(...middleware)
}
function configureStore(initial={}) {
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
rootReducer,
initial,
bindMiddleware([sagaMiddleware])
)
store.sagaTask = sagaMiddleware.run(rootSaga)
return store
}
export default configureStore
rootReducer.js
import { combineReducers } from 'redux'
import { cityReducer } from "./reducers/city"
import { doctorReducer } from "./reducers/doctor"
export default combineReducers({
city: cityReducer,
doctor: doctorReducer
})
city/reducer.js
import {actionTypes} from "../../actions/city"
const initialState = {
cities: []
}
export const cityReducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_CITIES:
return initialState
case actionTypes.FETCH_CITIES_SUCCESS:
return {
cities: action.data
}
case actionTypes.FETCH_CITIES_FAILURE:
return initialState
default:
return initialState
}
}
city/actions.js
export const actionTypes = {
FETCH_CITIES: 'FETCH_CITIES',
FETCH_CITIES_SUCCESS: 'FETCH_CITIES_SUCCESS',
FETCH_CITIES_FAILURE: 'FETCH_CITIES_FAILURE',
}
export const fetchCities = () => {
return {
type: actionTypes.FETCH_CITIES
}
}
export const fetchCitiesSuccess = (data) => {
return {
type: actionTypes.FETCH_CITIES_SUCCESS,
data
}
}
export const fetchCitiesFailure = () => {
return {
type: actionTypes.FETCH_CITIES_FAILURE
}
}
city/saga.js
import {call, put, takeLatest} from "redux-saga/effects"
import {actionTypes, fetchCitiesFailure, fetchCitiesSuccess} from "../../actions/city"
import {getCities} from "../../api"
export function* watchFetchCitiesRequest() {
yield takeLatest(actionTypes.FETCH_CITIES, workerFetchCitiesRequest)
}
function* workerFetchCitiesRequest() {
try {
const {data} = yield call(getCities)
yield put(fetchCitiesSuccess(data.results))
} catch (e) {
yield put(fetchCitiesFailure())
}
}
(!) For the doctors reducer/saga/actions the structure is the same except names.
Page.js is the main layout for every page. Basically wraps every page content in _document.js
const Page = ({dispatch}) => {
useEffect(() => {
dispatch(fetchCities())
}, [])
}
const mapStateToProps = ({city}) => ({cities: city.cities})
export default connect(mapStateToProps)(Page)
DoctorList.js Component that /doctor page consumes
import {useEffect} from "react"
import {fetchDoctors} from "../../actions/doctor"
import {getCity} from "../../utils/functions"
import {DoctorItemPreview} from "./DoctorItem"
const DoctorList = ({dispatch, isLoading, isError, response}) => {
useEffect(() => {
getCity() ? dispatch(fetchDoctors()) : null
},[getCity()])
return <>
{!isLoading ? <>
</> : <>
{[...Array(10)].map((e, i) => <span key={i}>
<DoctorItemPreview/>
</span>)}
</>}
</>
}
const mapStateToProps = ({doctor, city}) => ({
isLoading: doctor.isLoading,
isError: doctor.isError,
response: doctor.response,
})
export default connect(mapStateToProps)(DoctorList)
What can be the place where the problem appears? What parts of code do you need for the answer? Thanks
I am pretty sure your reducers will overwrite your current state when returning initialState. See this answer on GitHub.
There are no known race conditions or other problems related to multiple root stores nor reducers in next-redux-saga.

Redux - Reducer is not being called when I dispatch an action

I have a simple react app that fetches data from a JSON in my local and updates the state. The problem I face is that my reducer is not being called when I dispatch an action. I can see dispatch getting the response from the JSON but state is not updated
I did a console.log on reducer but no luck. I checked the Redux devtools but I don't see the state updated. Here is my code,
store.js
import {createStore, compose, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import {routerMiddleware} from 'react-router-redux'
import rootReducer from './combined.reducer'
import {createLogger} from 'redux-logger'
let middlewares = [
thunk
]
const logger = createLogger();
if (process.env.Node_ENV !== "production" && process.env.Node_ENV !== "test") {
middlewares = [
...middlewares,
logger,
require("redux-immutable-state-invariant").default()
]
}
export default function configureStore(history) {
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const enhancer = composeEnhancers(
applyMiddleware(...middlewares, routerMiddleware(history))
);
const store = createStore(
rootReducer(history),
enhancer
)
if (process.env.Node_ENV !== "production" && process.env.Node_ENV !== "test" && module.hot) {
module.hot.accept("./combined.reducer", () => {
const nextReducer = require("./combined.reducer").default
store.replaceReducer(nextReducer)
})
}
return store
combined.reducer.js
import { connectRouter } from 'connected-react-router';
import { combineReducers } from 'redux';
import * as workoutReducer from './features/WorkoutList/Containers/Workout.Reducer'
const createAppReducer = (history) => combineReducers({
router: connectRouter(history),
workoutReducer: workoutReducer
})
const rootReducer = (history) => (state, action) => {
return createAppReducer(history)(state,action)
};
export default rootReducer;
folder structure:
src
features
Workout
Components
Containers
Workout.Actions.js
Workout.Constants.js
Workout.Container.js
Workout.Reducer.js
Workout.js
Workout.Constants.js:
const constants = {
GET_WORKOUT_LIST_REQUEST: "GET_WORKOUT_LIST_REQUEST",
GET_WORKOUT_LIST_SUCCESS: "GET_WORKOUT_LIST_SUCCESS",
GET_WORKOUT_LIST_FAILURE: "GET_WORKOUT_LIST_FAILURE"
}
export default constants
Workout.Container.js:
import { connect } from 'react-redux'
import {withRouter} from 'react-router'
import {bindActionCreators} from 'redux'
import Workout from './Workout'
import * as workoutAction from './Workout.Actions'
export default withRouter(connect(
(state) => ({
workoutListData: state.workoutListData
}),
(dispatch) => ({
workoutActions:bindActionCreators(workoutAction, dispatch)
})
)(Workout))
Workout.Actions.js:
import constants from "./Workout.Constants"
import {getConfigProperty} from "../../../settings"
import {makeGetCall} from "../../../utils/Api"
const WORKOUT_LIST = getConfigProperty("workoutList")
export function getWorkoutList(url = WORKOUT_LIST) {
return (dispatch, getState) => {
dispatch(getWorkoutListRequest(true))
return makeGetCall(url, getState)
.then(res => res.json())
.then(json => {
dispatch(getWorkoutListSuccess(json))
dispatch(getWorkoutListRequest(false))
})
.catch(ex => {
dispatch(getWorkoutListFailure(ex))
dispatch(getWorkoutListRequest(false))
})
}
}
export function getWorkoutListRequest(req) {
return {
type: constants.GET_WORKOUT_LIST_REQUEST,
req
}
}
export function getWorkoutListSuccess(response) {
return {
type: constants.GET_WORKOUT_LIST_SUCCESS,
response
}
}
export function getWorkoutListFailure(exception) {
return {
type: constants.GET_WORKOUT_LIST_FAILURE,
exception
}
}
Workout.Reducer.js
import constants from "./Workout.Constants"
const initialState = {
workoutListData :{}
}
function reducer(state = initialState, action) {
console.log(".................I'm here................")
switch(action.type) {
case constants.GET_WORKOUT_LIST_SUCCESS: {
return Object.assign({}, state, {
workoutListData: action.response.GET_LIST_DATA
})
}
case constants.GET_WORKOUT_LIST_FAILURE: {
return Object.assign({}, state, initialState)
}
default:
return state
}
}
export {initialState}
export default reducer
workout.js:
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {WorkoutListItem} from './../StyledComponents/WorkoutListItem'
import {isEmpty, isEqual} from 'lodash'
import {getConfigProperty} from "../../../settings"
const ROWS_PER_PAGE = getConfigProperty("rowsPerPage")
export default class Workout extends Component {
static get PropTypes() {
return {
workoutListData: PropTypes.array.isRequired
}
}
constructor(props){
super(props)
this.state = {
workoutListData: []
}
}
componentWillMount(){
if(isEmpty(this.props.workoutListData)){
this.props.workoutActions.getWorkoutList()
}
}
loadWorkoutListItem = () => {
return <WorkoutListItem / >
}
render() {
return (
<div>
{this.loadWorkoutListItem()}
</div>)
}
}
The reducer function should have been called. I don't even see the console log printed
I don't think you want
import * as workoutReducer from './features/WorkoutList/Containers/Workout.Reducer'
but just
import workoutReducer from './features/WorkoutList/Containers/Workout.Reducer'
The first is importing the initialState and the reducer as a combined object, as opposed to just importing the reducer.
EDIT: this is in combined.reducer.js

React redux saga - getting some odd behaviour

I am in process of learning how to use reactjs, redux, react-redux and redux-saga. My attempt at this in my public github repo, found here:
https://github.com/latheesan-k/react-redux-saga/tree/5cede54a4154740406c132c94684aae1d07538b0
My store.js:
import { compose, createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import reducer from "./reducers";
import mySaga from "./sagas";
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers =
typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// TODO: Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
})
: compose;
const storeEnhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
export default createStore(reducer, storeEnhancer);
sagaMiddleware.run(mySaga);
my actions.js
export const HELLO_WORLD_REQUEST = "HELLO_WORLD_REQUEST";
export const HELLO_WORLD_RESPONSE = "HELLO_WORLD_RESPONSE";
export const HELLO_WORLD_ERROR = "HELLO_WORLD_ERROR";
export const helloWorldRequest = () => ({ type: HELLO_WORLD_REQUEST });
export const helloWorldResponse = text => ({ type: HELLO_WORLD_RESPONSE, text });
export const helloWorldError = error => ({ type: HELLO_WORLD_ERROR, error });
and my sagas.js
import { put, takeLatest } from "redux-saga/effects";
import { HELLO_WORLD_REQUEST, helloWorldResponse, helloWorldError } from "./actions";
function* runHelloWorldRequest(action) {
try {
// TODO: real api call here
yield put(helloWorldResponse("Hello from react-redux-saga :)"));
} catch (e) {
yield put(helloWorldError(e));
}
}
export default function* mySaga() {
yield takeLatest(HELLO_WORLD_REQUEST, runHelloWorldRequest);
}
and my helloWorldReducer.js
import { HELLO_WORLD_RESPONSE } from "../actions";
export default (state = "", { type, text }) => {
switch (type) {
case HELLO_WORLD_RESPONSE:
return text;
default:
return state;
}
};
and this is how put it all together on my App component:
class App extends Component {
componentDidMount() {
this.props.helloWorldRequest();
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>{this.props.responseText}</p>
</header>
</div>
);
}
}
const mapStateToProps = state => ({ responseText: state.helloWorldReducer });
const mapDispatchToProps = dispatch => bindActionCreators({ helloWorldRequest }, dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
This works fine, but here's the odd behaviour I am trying to understand. In order to get the response value out of the state and map it into props, I had to do this:
const mapStateToProps = state => ({ responseText:
state.helloWorldReducer });
Based on what I saw in the react devtools:
Notice after the request is processed and a response is generated; the resulting state object contains a field called helloWorldReducer.
Where did this come from?
I assumed this field name should have been called text.
P.S. Sorry about the long post; still learning react-redux-saga, so I didn't know which part of my code was relevant to the question at hand.
the resulting state object contains a field called helloWorldReducer.
Where did this come from?
It comes from your root reducer which is actually the reducer created by using the combineReducers() method.
This is your reducers/index.js file which export the root reducer for creating redux store:
import { combineReducers } from "redux";
import helloWorldReducer from "./helloWorldReducer";
export default combineReducers({
helloWorldReducer // here
});

How to dispatch multiple actions from redux action creators / thunk

In a small React/Redux app that I am writing, I have a thunk that looks as follow:
updateCategory(category){
return function(dispatch, getState){
dispatch({ type : types.UPDATE_CATEGORY, category });
dispatch({ type : locationTypes.UPDATE_CATEGORY_FOR_ALL_LOCATIONS, category });
}
}
complete code:
//Category Duck
import v4 from 'node-uuid';
import {types as locationTypes} from './locations'
export const types = {
UPDATE_CATEGORY:"UPDATE_CATEGORY"
};
const initialState = [];
export function reducer(state = initialState, action){
switch (action.type) {
case types.UPDATE_CATEGORY:
return state.map( item => item.id !== action.category.id ? item : {...action.category} );
default:
return state;
}
};
export const actions={
updateCategory(category){
return function(dispatch, getState){
dispatch({ type : types.UPDATE_CATEGORY, category });
dispatch({ type : locationTypes.UPDATE_CATEGORY_FOR_ALL_LOCATIONS, category });
}
}
}
//Location Duck
import v4 from 'node-uuid';
export const types = {
UPDATE_CATEGORY_FOR_ALL_LOCATIONS:"UPDATE_CATEGORY_FOR_ALL_LOCATIONS"
};
const initialState=[];
export function reducer(state = initialState, action){
switch (action.type) {
case types.UPDATE_CATEGORY_FOR_ALL_LOCATIONS:
return state.map(item => item.category.id !== action.category.id ? item : { ...item, 'category':{name:action.category.name, id:action.category.id} })
default:
return state;
}
};
export const actions={
updateCategoryForAllLocations(category){
return { type : types.UPDATE_CATEGORY_FOR_ALL_LOCATIONS, category}
}
}
//configStore
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import { routerReducer, routerMiddleware } from 'react-router-redux';//, push
import logger from 'redux-logger';
import * as storage from './localstorage';
import throttle from 'lodash/throttle';
import reducers from './duckes/rootReducer'; // Or wherever you keep your reducers
import thunk from 'redux-thunk';
const persistedState = storage.loadState();
const configureStore = (history) => {
const store = createStore(
combineReducers({
...reducers,
// router: routerReducer
}),
persistedState,
applyMiddleware(thunk)
);
store.subscribe(throttle(() => {
storage.saveState( store.getState() )
}),1000);
return store;
}
export default configureStore;
when calling it from a UI click handler in a connected component like this:
updateCategory({id:1, name:'foo')
I get back Error: Actions must be plain objects. Use custom middleware for async actions.
Can you advice me on how to solve this or maybe explain why it is happening?
the component with the call looks as follows:
/ManageCategoryPage
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {actions as categoryActions} from '../../duckes/categories';
import CategoryForm from './CategoryForm';
import {getElementByID} from '../../utils';
class ManageCategoryPage extends Component {
constructor(props) {
super(props);
//Init state
this.state = {
'category' : Object.assign({},this.props.category),
'errors':{}
}
//Bind functions
this.saveCategory=this.saveCategory.bind(this);
this.updateCategoryState=this.updateCategoryState.bind(this);
this.categoryExists=this.categoryExists.bind(this);
}
updateCategoryState(event){
...
}
categoryExists(category){
...
}
saveCategory(event){
event.preventDefault();
const {category}=this.state;
this.props.actions.updateCategory(category);
this.props.history.push('/categories');
}
//Render
render(){
return (
<CategoryForm
category={this.state.category}
locations={this.props.locations}
onChange={this.updateCategoryState}
onSave={this.saveCategory}
errors={this.state.errors}/>
)
}
}
//Prop Types validation
ManageCategoryPage.propTypes={
category: PropTypes.object.isRequired,
locations: PropTypes.array.isRequired,
categories: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired
};
//Redux connect
const mapStateToProps = ({locations, categories}, ownProps) => {
let category={id:'', name:''};
return {
category : getElementByID(categories, ownProps.match.params.id) || category,
locations : locations,
categories : categories
};
};
const mapDispatchToProps = (dispatch) => {
return {
'actions': bindActionCreators(categoryActions, dispatch)
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ManageCategoryPage);
I have found the issue and actually it was a mistake of mine, I had two configStore files and I somehow imported an old version of the configStore file that i created previously and it was missing 'thunk' as a param in applyMiddleware() call, so it means that it was applyMiddleware() instead applyMiddleware(thunk).

Resources