I'm creating a simple blog using React and Redux, most for learning these two libraries. Everything is working fine, my only question is about the way the state object is structured in the application. When I go to the Redux Toolkit, I see this:
Redux Toolkit screenshot
In the state I have a post object with another post object inside it. My question is: where did I defined the state in that way (with a post object inside a post object)? Below are the content of this application:
MainPost.js
import React, { useEffect } from 'react'
import { connect, useDispatch } from 'react-redux'
import { getPostListAction } from '../store/actions/getPostListAction'
export const MainPost = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(getPostListAction())
})
return (
<div>App</div>
)
}
const mapStateToProps = state => {
return {
post: state.post
}
}
export default connect(mapStateToProps)(MainPost)
store.js
import { createStore, applyMiddleware, compose } from 'redux'
import rootReducer from './reducers/rootReducer'
import { devToolsEnhancer } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
import { api } from './middleware/api'
const store = createStore(
rootReducer,
compose(
devToolsEnhancer(),
applyMiddleware(thunk),
)
)
export default store
postListReducer.js
const initialState = {
post: ''
}
export default function getPostListReducer(state = initialState, action) {
if(action.type === 'getPostList') {
return {
...state,
post: action.payload
}
} else {
return state
}
}
The first post (after state) is namespace of postListReducer
Here is how you use combineReducer to create a rootReducer:
const rootReducer = combineReducers({
post: postListReducer,
other: otherReducer
})
And to select data from the store, you do:
const mapStateToProps = state => {
return {
post: state.post.post // the first "post" (after "state") is namespace of postListReducer
}
}
Or if you don't want to write state.post.post, you can change your postListReducer to directly hold the "post" data:
const initialPost = ''
export default function getPostListReducer(state = initialPost, action) {
if(action.type === 'getPostList') {
return action.payload
} else {
return state
}
}
Related
I am new to Next.js, So I follow some tutorials for Redux integration in Next.js. All is working fine but whenever I switch between pages, each time API make a call, and Redux lost its stored value.
The basic function is like this. Whenever a user loads a website an API call will fetch category data from the server and save that data in reducer[categoryReducer], then the user can navigate to any page and category data will fetched from the reducer. But in my case, it hits again and again
Full Code:
// Action Call
import * as Constants from '../../constant/constant';
import * as t from '../types';
import axios from 'axios';
export const loadCategoryApi = (type) => dispatch => {
axios.post(Constants.getCategories,type)
.then(function (response) {
console.log(response);
if(response && response.data && response.data.status==="200"){
dispatch({
type: t.LOAD_CATEGORY,
value: type
});
}
else if(response && response.data && response.data.status==="404"){
alert('Someting went wrong');
}
})
}
// Reducer File
import * as t from '../types';
const initialState = {
doc:null
}
const CategoryReducer = (state = initialState, action) =>{
console.log('reducer action', action.type);
switch (action.type){
case t.LOAD_CATEGORY:
console.log('slots actions',action);
return({...state, doc:action.value})
default:
return state;
}
}
export default CategoryReducer;
// Store file
import { createStore, applyMiddleware, compose } from "redux"
import thunk from "redux-thunk"
import { createWrapper } from "next-redux-wrapper"
import rootReducer from "./reducers/rootReducer"
const middleware = [thunk]
const makeStore = () => createStore(rootReducer, compose(applyMiddleware(...middleware)))
export const wrapper = createWrapper(makeStore);
// rootReducer
import { combineReducers } from "redux"
import CategoryReducer from "./CategoryReducer";
const rootReducer = combineReducers({
CategoryReducer: CategoryReducer
})
export default rootReducer;
// _app.js
import React from "react"
import { wrapper } from "../redux/store"
import Layout from '../components/Layout';
import '../styles/globals.css'
const MyApp = ({ Component, pageProps }) =>(
<Layout>
<Component {...pageProps} />
</Layout>
);
export default wrapper.withRedux(MyApp);
// Uses
import React, { useState, useEffect } from 'react';
import {connect} from "react-redux";
import {loadCategoryApi} from "../redux/actions/CategoryAction";
function navbar(props){
const { loadCategory, loadCategoryApi } = props;
useEffect(() => {
if(loadCategory===null){
console.log('navbar loading funciton');
loadCategoryFunation();
}
}, []);
const loadCategoryFunation = () =>{
var json = {
type : 'main'
};
loadCategoryApi(json);
}
}
const mapStateToProps = state => {
return { loadCategory: state.CategoryReducer.doc }
}
const mapDispatchToProps = {
loadCategoryApi
}
export default connect(mapStateToProps, mapDispatchToProps)(Navbar)
What I am doing wrong?
You have to create main reducer to handle the hydration. I explained this hydration process here.
In the file that you created the store, write main reducer
import reducers from "./reducers/reducers";
const reducer = (state, action) => {
// hydration is a process of filling an object with some data
// this is called when server side request happens
if (action.type === HYDRATE) {
const nextState = {
...state,
...action.payload,
};
return nextState;
} else {
// whenever we deal with static rendering or client side rendering, this will be the case
// reducers is the combinedReducers
return reducers(state, action);
}
};
then pass this reducer to the store
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 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
I'm trying to add Redux to my test app but I'm having trouble fetching data from my API. It feels like I've gone through all of the steps but I can't access the props from the fetch in my component, so I'm messing up somewhere along the way. My code:
actions/index.js:
import 'whatwg-fetch';
import ReduxThunk from 'redux-thunk';
export const FETCH_RECIPES = 'FETCH_RECIPES';
const ROOT_URL = 'http://myapi/recipe/';
export function fetchRecipes(id) {
const url = ROOT_URL + "0";
// const request = fetch(url);
return (dispatch) => {
fetch(url)
.then((response) => response.json())
.then((request) => dispatch(fetchRecipesSuccess(request)))
};
}
export function fetchRecipesSuccess(request) {
return {
type: FETCH_RECIPES,
request
};
}
reducer_recipe.js:
import { FETCH_RECIPES } from "../actions/index";
export default function(state = [], action) {
switch(action.type) {
case FETCH_RECIPES:
return [action.payload.data, ...state];
}
return state;
}
reducers/index.js:
import { combineReducers } from 'redux';
import RecipesReducer from './reducer_recipes';
const rootReducer = combineReducers({
recipes: RecipesReducer
})
export default rootReducer;
aaaaand in my component I'm using this code:
function mapStateToProps({ recipes }) {
return { recipes };
}
connect(mapStateToProps, {fetchRecipes})(Recipe);
And in my index.js I'm creating my store with const createStoreWithMiddleware = createStore(reducers, applyMiddleware(ReduxPromise));
With my thinking I should be good to use this.props to access the data I've fetched from my API but I guess I'm dropping the data somewhere along the way. What am I missing?
Check your reducer well. You seem to be returning action.payload.data whereas in your fetchRecipesSuccess, it's named request. And you can console.log the action object to see what you've got
import { FETCH_RECIPES } from "../actions/index";
export default function(state = [], action) {
switch(action.type) {
case FETCH_RECIPES:
// Verify here that your request object has data
return [...state, action.request.data];
// Default state
default:
return state;
}
}
Hope this helps!
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.