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
});
Related
I'm tried to use React Hooks form and pass the values to the state from the Redux store through my setLog() action, and as I understand my action doesn't work but not getting through any type of error. So, I don't know what exactly is going wrong. Also, I use redux-saga, but for now, I don't use it, I just declared it, it could be the issue or not?
SignUp.js file
import React from "react";
import { useForm } from "react-hook-form";
import { connect } from "react-redux";
import { setLog } from "../actions";
function SignUp({ info }) {
const { register, handleSubmit, setValue } = useForm();
const onSubmit = data => setLog(data);
return (
<React.Fragment>
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("login")} />
<input {...register("password")} />
<input type="submit" />
</form>
{/* <h1>{info}</h1> */}
</React.Fragment>
);
}
const mapStateToProps = (state) => {
return {
info: state.signUp
};
}
export default connect(mapStateToProps, { setLog })(SignUp);
Actions:
export const setLog = (data) => {
return {
type: 'SET_LOG',
payload: data
};
}
SignUp reducer
export default(state=[],action) => {
switch(action.type){
case 'SET_LOG':
return [...state, ...action.payload];
default:
return state;
}
}
My store:
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas';
import rootReducer from '../reducers';
const configureStore = () => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__
? compose(
applyMiddleware(sagaMiddleware),
window.__REDUX_DEVTOOLS_EXTENSION__(),
)
: applyMiddleware(sagaMiddleware),
);
sagaMiddleware.run(rootSaga);
return store;
};
export default configureStore;
What is happening is that you're not calling the setLog from the props, you're just calling the function without tying it with redux, you need to attach the function using the mapDispatchToProps function and calling the dispatch inside that function like this:
const mapDispatchToProps = (dispatch) => {
return {
// Calling it submitLog to avoid name collisions with the imported setLog
submitLog: (data) => dispatch(setLog(data)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SignUp);
The code above is connecting the action to redux, now you just need to call it as a prop like this:
// Now you will have the submitLog as a prop and it will be tie to redux.
function SignUp({ info, submitLog }) {
const { register, handleSubmit, setValue } = useForm();
const onSubmit = data => submitLog(data);
// Rest of the code
}
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 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;
I am having a react-redux action creation problem. My props are an empty object when I log props at the life cycle method componentDidMount()
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchSurveys } from '../../actions/index';
export class SurveyList extends Component {
componentDidMount() {
console.log(this.props);
this.props.fetchSurveys();
}
renderSurveys() {
return (
this.props.surveys.length &&
this.props.surveys.map(survey => {
return (
<div className="card blue-grey darken-1" key={survey._id}>
<div className="card-content">
<span className="card-title">{survey.title}</span>
<p>{survey.body}</p>
<p className="right">
Sent On: {new Date(survey.dateSent).toLocaleDateString()}
</p>
</div>
<div className="card-action">
<a>Yes: {survey.yes}</a>
<a>No: {survey.no}</a>
</div>
</div>
);
})
);
}
render() {
return <div>{this.renderSurveys()}</div>;
}
}
function mapStateToProps({ surveys }) {
return { surveys };
}
export default connect(mapStateToProps, { fetchSurveys })(SurveyList);
Now according to the react-redux docs by default dispatch is included on the props so we do not need to explicitly call a mapDispatchToProps in the connect method in order to hit our action creators. fetchSurveys() is an action creator and I expect it to return a list of surveys which I then render.
However this.props = {}; so of course I cannot call .map on undefined at renderSurveys() as I do not get the surveys property on props either.
I am really troubled by why my props are empty. Can anybody shed some light onto this problem, I would be very grateful. I have tried using bindActionCreators and having my own mapDispatchToProps method, this doesn't work either.
Here are my actions.
import axios from 'axios';
import { FETCH_USER, FETCH_SURVEYS } from './types';
export const fetchUser = () => async dispatch => {
const res = await axios.get('/api/current_user');
dispatch({ type: FETCH_USER, payload: res.data });
};
export const handleToken = token => async dispatch => {
const res = await axios.post('/api/stripe', token);
dispatch({ type: FETCH_USER, payload: res.data });
};
export const submitSurvey = (values, history) => async dispatch => {
const res = await axios.post('/api/surveys', values);
history.push('/surveys');
dispatch({ type: FETCH_USER, payload: res.data });
};
export const fetchSurveys = () => async dispatch => {
console.log('called');
const res = await axios.get('/api/surveys');
dispatch({ type: FETCH_SURVEYS, payload: res.data });
};
My surveys reducer -
import { FETCH_SURVEYS } from '../actions/types';
export default function(state = [], action) {
switch (action.type) {
case FETCH_SURVEYS:
return action.payload;
default:
return state;
}
}
Props in console -
Combined reducer -
import { combineReducers } from 'redux';
import authReducer from './authReducer';
import { reducer as reduxForm } from 'redux-form';
import surveysReducer from './surveysReducer';
export default combineReducers({
auth: authReducer,
form: reduxForm,
surveys: surveysReducer
});
Just saw your code in the link. In Dashboard.js
Shouldn't
import { SurveyList } from './surveys/SurveyList';
be
import SurveyList from './surveys/SurveyList';
Since the connected component is exported as default?
Have you used your reducer in combineReducers? you should have an index.js file in reducers folder (for example) which has something like this:
import {myreducer} from './myreducer';
const rootReducer = combineReducers(
{
myreducer: myreducer
}
);
export default rootReducer;
Do you have complete codebase somewhere? It looks like issue is not in provided code..
Seems that you forgot to add your reducer to main reducers file.
So, it depends on your code it should be:
// reducers/index.js
import { combineReducers } from 'redux';
import surveys from './surveysReducer';
const rootReducer = combineReducers({
surveys,
});
export default rootReducer;
Hope it will helps.