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).
Related
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.
I am new to react and I am making a little business directory as I'm learning.
I've made a very basic component such as:
BusinessDirectory.js
export class BusinessDirectory extends React.Component {
componentWillMount() {
console.log(this.props);
}
render() {
return (<div><h1>Business Directory</h1></div>)
}
}
const mapStateToProps = state => ({
businesses: state.businesses.items
});
export default connect(mapStateToProps, { fetchBusinesses })(BusinessDirectory);
App.js
import React from "react";
import { Provider } from "react-redux";
import { BusinessDirectory } from "./components/BusinessDirectory";
import store from "./store";
export class App extends React.Component {
render() {
return (
<Provider store={store}>
<BusinessDirectory />
</Provider>
);
}
}
export default App;
store.js
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
applyMiddleware(...middleware)
);
export default store;
reducers/index.js
import { combineReducers } from "redux";
import businessReducer from "./business.reducer";
export default combineReducers({
businesses: businessReducer
});
reducers/business.reducer.js
import { FETCH_BUSINESS, FETCH_BUSINESSES } from "../actions/types";
const initialState = {
items: []
};
export default function(state = initialState, action) {
switch (action.type) {
case FETCH_BUSINESSES:
return {
...state,
items: action.payload
};
break;
default:
return state;
}
}
business.action.js
import { FETCH_BUSINESSES } from "./types";
export const fetchBusinesses = () => dispatch => {
fetch("http://localhost:5000/businesses")
.then(res => res.json())
.then(data =>
dispatch({
type: FETCH_BUSINESSES,
payload: data.data
})
);
};
The issue I am having is that this.props inside of the BusinessDirectory is an empty object so I cannot call the methods that I am creating. I've tried to do it by doing the mapDispatchToProp but that also doesn't work.
The problem is that in App.js you import the wrong component:
import { BusinessDirectory } from "./components/BusinessDirectory";
will import the so called named export, i.e. what you defined as
export class BusinessDirectory extends React.Component {
This is a class that assumes that "someone" passes the props to it.
On the other hand,
import BusinessDirectory from "./components/BusinessDirectory";
would import the default export, which is the connected component
export default connect(mapStateToProps, { fetchBusinesses })(BusinessDirectory);
i.e. a component that actually takes care of passing the props from redux.
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
When authentication is done I can see the actions updating and the reducer updating - but then mapstatetoprops does not do anything with the new reducer state
The Store keeps logging the same state(initial state) on every update
import React from 'react';
import { bindActionCreators } from 'redux'
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth';
import { Redirect } from 'react-router-dom'
import firebase from 'firebase';
import { connect } from 'react-redux';
import { signIn, signOut } from '../reducer/actions'
import { auth } from '../firebase'
class LoginPage extends React.PureComponent {
// Configure FirebaseUI.
uiConfig = {'FirebaseUI Config'}
componentDidMount = () => {
auth.onAuthStateChanged((user) => { // gets user object on authentication
console.log('OnAuthStateChanged', user)
console.log('Check If Props Change in AuthChange', this.props.isauthed)
if (user) {
this.props.signIn(user)
} else {
this.props.signOut(user)
}
});
}
render() {
console.log('Check If Props Change in Render', this.props.isauthed)
if (!this.props.isauthed) {
return (
<div>
<h1>My App</h1>
<p>Please sign-in:</p>
<StyledFirebaseAuth uiConfig={this.uiConfig} firebaseAuth={firebase.auth()} />
</div>
);
}
return (
<Redirect to='/' />
)
}
}
export default (LoginPage);
JS that should dispatch and update the props
import { connect } from 'react-redux';
import { signIn, signOut } from '../reducer/actions'
import { bindActionCreators } from 'redux'
import LoginPage from './LoginPage';
const mapStateToProps = (state) => {
console.log('LOGINmapstatetoprops', state.Authed)
return {
isauthed: state.Authed.isauthed,
}
}
const mapDispatchToProps = (dispatch) => {
console.log('LOGINmapDISPATCHoprops')
return bindActionCreators({signIn, signOut}, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginPage);
The reducer
import LoginPage from '../components/LoginPage';
import firebase from 'firebase';
const initialState = {
isauthed: false,
error: ''
}
const AuthReducer = (state = initialState, action) => {
console.log('this is an action', action)
switch (action.type) {
case 'IsSignedIn':
return {
...state,
isauthed: action.payload
}
break;
case 'IsNotSignedIn':
return {
...state,
isauthed: action.payload
}
break;
default: return state
}
}
export default AuthReducer;
This is the actions file
export const signIn = (user) => {
console.log('this is from actions', user)
return {
type: 'isSignedIn',
payload: true
}
}
export const signOut = (user) => {
console.log(user)
return {
type: 'isNotSignedIn',
payload: false
}
}
Redux Store
import { createStore } from 'redux';
import logger from 'redux-logger';
import { createEpicMiddleware } from 'redux-observable';
import {AllReducers} from './reducer/index';
//import rootEpic from './modules/rootEpic';
//const epicMiddleware = createEpicMiddleware(rootEpic);
const store = createStore(AllReducers);
store.subscribe(() => {
console.log('storeupdated', store.getState())
});
export default store;
CombineReducers Function
import {combineReducers} from 'redux'
import {AuthReducer} from './AuthReducer'
import {UserReducer} from './UserReducer'
export const AllReducers = combineReducers({
Authed: AuthReducer
User: UserReducer
})
Seems like you have a typo error between 'isSignedIn' and 'IsSignedIn' (first letter differ)
You could avoid that by using a variable declared in a separated file. I like to call it types.js for the types of actions:
// types.js
export const IS_SIGNED_IN = 'isSignedInOrEvenWhateverButYouBetterKeepAClearName'
// (same with all your types)
And then you can import this variable in your other files. You won't have typos anymore!
P.S: you can find all these ideas in redux documentation
I try to deal with ajax data in my learning react,redux project and I have no idea how to dispatch an action and set the state inside a component
here is my component
import React, {PropTypes, Component} from 'react';
import Upload from './Upload';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as profileActions from '../../../actions/profileActions';
class Profile extends Component {
static propTypes = {
//getProfile: PropTypes.func.isRequired,
//profile: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {
profile:{
username: '',
password: ''
}
}
this.onUpdate = this.onUpdate.bind(this)
}
onUpdate(event) {
alert()
}
componentWillMount() {
}
componentDidMount() {
}
render() {
const {profile} = this.props;
return (
<div>
</div>
);
}
}
function mapStateToProps(state) {
console.log(state)
return {
profile: state.default.profile,
};
}
function mapDispatchToProps(dispatch, ownProps) {
return {
actions: bindActionCreators(profileActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Profile);
I create the store as it follows
import { createStore, combineReducers, applyMiddleware } from 'redux'
import createLogger from 'redux-logger'
import thunk from 'redux-thunk'
import { routerReducer, routerMiddleware, push } from 'react-router-redux'
import reducers from '../reducers'
import { browserHistory } from 'react-router';
const middleware = [ thunk ];
if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger());
}
middleware.push(routerMiddleware(browserHistory));
// Add the reducer to your store on the `routing` key
const store = createStore(
combineReducers({
reducers,
routing: routerReducer
}),
applyMiddleware(...middleware),
)
export default store;
reducer
export const RESOLVED_GET_PROFILE = 'RESOLVED_GET_PROFILE'
const profileReducer = (state = {}, action) => {
switch (action.type) {
case 'RESOLVED_GET_PROFILE':
return action.data;
default:
return state;
}
};
export default profileReducer;
actions
import * as types from './actionTypes';
import Api from '../middleware/Api';
export function getProfile() {
return dispatch => {
dispatch(setLoadingProfileState()); // Show a loading spinner
Api.getAll('profile').then(profile => {
dispatch(doneFetchingProfile(profile));
}).catch(error => {
throw(error);
});
/*Api.fetch(`profile`, (response) => {
console.log(response)
dispatch(doneFetchingBook()); // Hide loading spinner
if(response.status == 200){
dispatch(setProfile(response.json)); // Use a normal function to set the received state
}else {
dispatch(error)
}
}) */
}
}
function setProfile(data) {
return {type: types.SET_PROFILE, data: data}
//return { type: types.SET_PROFILE, data: data };
}
function setLoadingProfileState() {
return {type: types.SHOW_SPINNER}
}
function doneFetchingProfile(data) {
console.log(data)
return {
type: types.HIDE_SPINNER,
profile: data
}
}
function error() {
return {type: types.SHOW_ERROR}
}
but I have no idea how would I dispatch action and update the state after getProfile action
You need to only dispatch your event RESOLVED_GET_PROFILE right after dispatching doneFetchingProfile, or simply listen RESOLVED_GET_PROFILE and hide spinner on reducing it.
Api.getAll('profile').then(profile => {
dispatch(doneFetchingProfile(profile));
dispatch(resoloveGetProfile(profile));
})
Actually you r doing everything right - so I didn't understand what is your question is, so if you meant something else - let me know, I`ll try to describe you.
About dispatch(resoloveGetProfile(profile));
There you dispatch action, which will update your state, simple as you do with some static state, I saw that you already have setProfile action, so you can change that line, to call your existed function.
dispatch(setProfile(profile))
Than you need to reduce your state in this action
case 'SET_PROFILE' : (action, state) => {...state, profile: action.data}
Than your state will change and your components will update. Note that your 'get profile method' better to call from componentDidMount to avoid freezing at rendering because of performing web request.