I've got a sample website with redux(redux-saga,redux-next-wrapper).
I created all components of redux and configured them. But I got an error when dispatching in getStaticProps with title `TypeError: nextCallback is not a function. I will share my structures code and thanks to finding my problem.
Action code
import { NewsActionE } from "../../../enums/newsActionEnum"
import { FetchGetsRequest, FetchNewsSuccessPayload, FetchPostsFailure, FetchPostsFailurePayload, FetchPostsSuccess } from "../../../types/allNewsT"
export const requestNews = (): FetchGetsRequest => ({
type: NewsActionE.REQUESTNEWS
})
export const fetchNewsSuccess = (
payload: FetchNewsSuccessPayload
): FetchPostsSuccess => ({
type: NewsActionE.GETALLNEWS,
payload
});
export const ErrorNews = (
payload: FetchPostsFailurePayload
): FetchPostsFailure => ({
type: NewsActionE.Failure,
payload
});
reducers
import { HYDRATE } from "next-redux-wrapper";
import { AnyAction } from "redux";
import { NewsActionE } from "../../../enums/newsActionEnum"
export interface initialState {
pending: false
errors: null
articles: []
}
export const reducer = (state: initialState | undefined, action: AnyAction): initialState | any => {
switch (action.type) {
case HYDRATE:
// Attention! This will overwrite client state! Real apps should use proper reconciliation.
return { ...state, ...action.payload };
case NewsActionE.REQUESTNEWS:
return {
...state,
pending: true,
}
case NewsActionE.Failure:
return {
...state,
pending: false,
articles: [],
errors: action.payload.console.error
}
case NewsActionE.GETALLNEWS:
return {
...state,
pending: false,
articles: action.payload,
errors: null
}
default:
return { ...state };
}
}
sagas
import axios, { AxiosResponse } from "axios"
import http from '../../../services/httpService';
import { call, put, all, takeLatest } from 'redux-saga/effects'
import { News } from '../../../models/news/News'
import { ErrorNews, fetchNewsSuccess } from "../..";
import { NewsActionE } from "../../../enums/newsActionEnum";
const getNews = (): Promise<AxiosResponse<News, any>> =>
http.get<News>('/data/getallposts')
function* fetchNewsSaga(): any {
try {
const response = yield call(getNews);
yield put(fetchNewsSuccess({
news: response.data
}));
} catch (e) {
yield put(ErrorNews({
error: 'e.message'
}));
}
}
function* newsSaga() {
yield all([takeLatest(NewsActionE.REQUESTNEWS, fetchNewsSaga)])
}
export default newsSaga;
root saga
import { all, fork } from "#redux-saga/core/effects";
import newsSaga from "./news/newssaga";
export function* RootSaga() {
yield all([fork(newsSaga)])
}
store
import { AnyAction, applyMiddleware, createStore, Store } from "redux";
import reducers from "../reducer/news";
import createSagaMiddleware from "#redux-saga/core";
import { Task } from 'redux-saga'
import { RootSaga } from "../sagas/rootSaga";
import { Context, createWrapper } from "next-redux-wrapper";
import { initialState } from "../reducer/news/newsReducer";
import { RootState } from "..";
export interface SagaStore extends Store<initialState | any, AnyAction> {
sagaTask?: Task;
}
export const makeStore = (context: Context) => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducers,
applyMiddleware(sagaMiddleware));
(store as SagaStore).sagaTask = sagaMiddleware.run(RootSaga);
return store;
};
export const wrapper = createWrapper<Store<RootState>>(makeStore, { debug: true });
index.ts
import { END } from '#redux-saga/core'
import type { NextPage } from 'next'
import Layout from '../components/Layout'
import AllNews from '../components/news'
import { requestNews, wrapper } from '../state'
const Home: NextPage = () => {
return (
<Layout title="News site">
<p>News site Home page</p>
<AllNews />
</Layout>
)
}
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
store.dispatch(requestNews());
// end the saga
store.dispatch(END);
await store.sagaTask.toProm();
})
export default Home
You are exporting your reducer as a named export, but import it as a default import. As a result, it will likely be undefined.
Do
import { reducer } from "../reducer/news";
instead of
import reducers from "../reducer/news";
Problem
Hello friends,
I'm developing an app with react + redux, the problem I have is that the series property that comes from
const mapStateToProps = (state) => {
return {
series: state.serieReducer
}
}
It is showing me undefined in console, but if you see the image in the action it loads the 30 data in the array.
I would like if you could help me to solve the problem and be able to load the data in the render(){} function.
src/components/Home/index.js
import React, {Component} from 'react';
import Page from './page.js';
import fetchSeriesAction from '../../redux/actions/series/action.fetch.js';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux'
class Home extends Component {
componentDidMount() {
this.props.fetchSeries();
}
render(){
console.log('TESTING SERIES LIST:', this.props) // show undefined
return(
<Page/>
)
}
}
const mapStateToProps = (state) =>{
return{
series: state.serieReducer
}
}
const mapDispatchToProps = dispatch => bindActionCreators({
fetchSeries: fetchSeriesAction
}, dispatch)
export default connect(mapStateToProps , mapDispatchToProps)(Home);
src/redux/actions/series
serie/action.error.js
import {FETCH_SERIES_ERROR} from '../../types.js';
const fetchSeriesError = (error) =>{
return {
type: FETCH_SERIES_ERROR,
error: error
}
}
export default fetchSeriesError;
series/action.pending.js
import {FETCH_SERIES_PENDING} from '../../types.js';
const fetchSeriesPending = () =>{
return {
type: FETCH_SERIES_PENDING
}
}
export default fetchSeriesPending;
series/action.success.js
import {FETCH_SERIES_SUCCESS} from '../../types.js';
const fetchSeriesSuccess = (series) =>{
return {
type: FETCH_SERIES_SUCCESS,
series: series
}
}
export default fetchSeriesSuccess;
series/action.fetch.js
import fetchSeriesPending from './action.pending.js';
import fetchSeriesSuccess from './action.sucess.js';
import fetchSeriesError from './action.error.js';
const fetchData = () =>{
return async dispatch => {
dispatch(fetchSeriesPending());
await fetch('https://cinemanight.chrismichael.now.sh/api/v1/series/1')
.then(res => res.json())
.then(res => {
if(res.error) {
throw(res.error);
}
dispatch(fetchSeriesSuccess(res.series));
return res.series;
})
.catch(error => {
dispatch(fetchSeriesError(error));
})
}
}
export default fetchData;
reducers/series
series/result.js
import {
FETCH_SERIES_ERROR,
FETCH_SERIES_PENDING,
FETCH_SERIES_SUCCESS
} from '../../types.js';
const defaultState = {
pending: true,
series: [],
error: null
}
const reducer = (state = defaultState, action) =>{
switch(action.type){
case FETCH_SERIES_PENDING:
return{
... state,
pending: true
}
case FETCH_SERIES_SUCCESS:
return{
... state,
pending: false,
series: action.payload
}
case FETCH_SERIES_ERROR:
return{
... state,
pending: false,
error: action.error
}
default:
return state;
}
}
export default reducer;
series/index.js
import resultReducer from './result.js';
export default {
resultReducer
}
store.js
import {createStore , combineReducers , applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import SeriesReducer from './reducers/series/index.js';
const middlewares = [thunk , logger];
const reducers = combineReducers({
... SeriesReducer
});
const store = createStore(reducers , applyMiddleware(... middlewares));
export default store;
You need to fix either action.success.js or your reducer because in reducer you are setting action.payload and you are sending series from your action.success.js
Either change your action.success.js to:
import {FETCH_SERIES_SUCCESS} from '../../types.js';
const fetchSeriesSuccess = (series) =>{
return {
type: FETCH_SERIES_SUCCESS,
payload: series
}
}
export default fetchSeriesSuccess;
or change your reducer like this:
import {
FETCH_SERIES_ERROR,
FETCH_SERIES_PENDING,
FETCH_SERIES_SUCCESS
} from '../../types.js';
const defaultState = {
pending: true,
series: [],
error: null
}
const reducer = (state = defaultState, action) =>{
switch(action.type){
case FETCH_SERIES_PENDING:
return{
... state,
pending: true
}
case FETCH_SERIES_SUCCESS:
return{
... state,
pending: false,
series: action.series
}
case FETCH_SERIES_ERROR:
return{
... state,
pending: false,
error: action.error
}
default:
return state;
}
}
export default reducer;
update your mapStateToProps method as well:
const mapStateToProps = (state) => {
return {
series: state.resultReducer
}
}
I've got this working but i'm after a more 'best practice way'.
its using the https://icanhazdadjoke api to display a random joke that gets updated every x seconds. is there a better way of doing this?
eventually i want to add stop, start, reset functionality and feel this way might not be the best.
Any middleware i can use?
Redux actions
// action types
import axios from 'axios';
export const FETCH_JOKE = 'FETCH_JOKE';
export const FETCH_JOKE_SUCCESS = 'FETCH_JOKE_SUCCESS';
export const FETCH_JOKE_FAILURE = 'FETCH_JOKE_FAILURE';
function fetchJoke() {
return {
type: FETCH_JOKE
};
}
function fetchJokeSuccess(data) {
return {
type: FETCH_JOKE_SUCCESS,
data
};
}
function fetchJokeFail(error) {
return {
type: FETCH_JOKE_FAILURE,
error
};
}
export function fetchJokeCall(){
return function(dispatch){
dispatch(fetchJoke());
return axios.get('https://icanhazdadjoke.com', { headers: { 'Accept': 'application/json' }})
.then(function(result){
dispatch(fetchJokeSuccess(result.data))
})
.catch(error => dispatch(fetchJokeFail(error)));
}
}
Redux reducer
import {combineReducers} from 'redux';
import {FETCH_JOKE, FETCH_JOKE_SUCCESS, FETCH_JOKE_FAILURE} from '../actions';
const defaultStateList = {
isFetching: false,
items:[],
error:{}
};
const joke = (state = defaultStateList, action) => {
switch (action.type){
case FETCH_JOKE:
return {...state, isFetching:true};
case FETCH_JOKE_SUCCESS:
return {...state, isFetching:false, items:action.data};
case FETCH_JOKE_FAILURE:
return {...state, isFetching:false, error:action.data};
default:
return state;
}
};
const rootReducer = combineReducers({
joke
});
export default rootReducer;
Joke component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchJokeCall } from '../actions';
class Joke extends Component {
componentDidMount() {
this.timer = setInterval(()=> this.props.fetchJokeCall(), 1000);
}
componentWillUnmount() {
clearInterval(this.timer)
this.timer = null;
}
render() {
return (
<div>
{this.props.joke.joke}
</div>
);
}
}
Joke.propTypes = {
fetchJokeCall: PropTypes.func,
joke: PropTypes.array.isRequired
};
function mapStateToProps(state) {
return {
joke: state.joke.items,
isfetching: state.joke.isFetching
};
}
export default connect(mapStateToProps, { fetchJokeCall })(Joke);
Redux-Sagas is better and we are using it in our applications as well, this is how you can poll using Redux-Sagas
Just to give you an idea this is how you can do it, You also need to understand how Redux-Sagas work
Action
export const FETCH_JOKE = 'FETCH_JOKE';
export const FETCH_JOKE_SUCCESS = 'FETCH_JOKE_SUCCESS';
export const FETCH_JOKE_FAILURE = 'FETCH_JOKE_FAILURE';
export const START_POLLING = 'START_POLLING';
export const STOP_POLLING = 'STOP_POLLING';
function startPolling() {
return {
type: START_POLLING
};
}
function stopPolling() {
return {
type: STOP_POLLING
};
}
function fetchJoke() {
return {
type: FETCH_JOKE
};
}
function fetchJokeSuccess(data) {
return {
type: FETCH_JOKE_SUCCESS,
data
};
}
function fetchJokeFail(error) {
return {
type: FETCH_JOKE_FAILURE,
error
};
}
Reducer
import {combineReducers} from 'redux';
import {FETCH_JOKE, FETCH_JOKE_SUCCESS, FETCH_JOKE_FAILURE, START_POLLING, STOP_POLLING } from '../actions';
const defaultStateList = {
isFetching: false,
items:[],
error:{},
isPolling: false,
};
const joke = (state = defaultStateList, action) => {
switch (action.type){
case FETCH_JOKE:
return {...state, isFetching:true};
case FETCH_JOKE_SUCCESS:
return {...state, isFetching:false, items:action.data};
case FETCH_JOKE_FAILURE:
return {...state, isFetching:false, error:action.data};
case START_POLLING:
return {...state, isPolling: true};
case STOP_POLLING:
return {...state, isPolling: false};
default:
return state;
}
};
const rootReducer = combineReducers({
joke
});
export default rootReducer;
Sagas
import { call, put, takeEvery, takeLatest, take, race } from 'redux-saga/effects'
import {FETCH_JOKE, FETCH_JOKE_SUCCESS, FETCH_JOKE_FAILURE, START_POLLING, STOP_POLLING } from '../actions';
import axios from 'axios';
function delay(duration) {
const promise = new Promise(resolve => {
setTimeout(() => resolve(true), duration)
})
return promise
}
function* fetchJokes(action) {
while (true) {
try {
const { data } = yield call(() => axios({ url: ENDPOINT }))
yield put({ type: FETCH_JOKE_SUCCESS, data: data })
yield call(delay, 5000)
} catch (e) {
yield put({ type: FETCH_JOKE_FAILURE, message: e.message })
}
}
}
function* watchPollJokesSaga() {
while (true) {
const data = yield take(START_POLLING)
yield race([call(fetchJokes, data), take(STOP_POLLING)])
}
}
export default function* root() {
yield [watchPollJokesSaga()]
}
You can also use Redux-Observable, if you want to get more into this read this article
I've been working on pretty much the same problem, except that I wasn't concerned about starting and stopping the poll. For some reason the while loop kept freezing my app so I dispensed of it and instead set up my saga like this.
import { all, takeLatest, call, put } from 'redux-saga/effects';
import axios from 'axios';
import { API_CALL_REQUEST, API_CALL_SUCCESS, API_CALL_FAILURE, API_CALL_FETCHED } from
'../actions/giphy';
function apiFetch() {
let randomWord = require('random-words');
let API_ENDPOINT = `https://api.giphy.com/v1/gifs/search?
api_key=MYKEY&q=${randomWord()}&limit=12`;
return axios({
method: "get",
url: API_ENDPOINT
});
}
export function* fetchImages() {
try {
const res = yield call(apiFetch)
const images = yield res.data
yield put({type: API_CALL_SUCCESS, images})
} catch (e) {
yield put({type: API_CALL_FAILURE, e})
console.log('Error fetching giphy data')
}
}
export default function* giphySaga() {
yield all([
takeLatest(API_CALL_REQUEST, fetchImages),
]);
}
Then inside my component I added this.
componentDidMount() {
this.interval = setInterval(() => {
this.props.dispatch({type: 'API_CALL_REQUEST'});
}, 5000);
}
componentWillUnmount() {
clearInterval(this.interval)
}
It's working, but would like some feedback on how this could be possibly improved.
Here's a poor man's way. I don't think it's the best way but it doesn't require any extra library.
Actions
// action types
import axios from 'axios';
export const START_POLLING_JOKE = 'START_POLLING_JOKE';
export const STOP_POLLING_JOKE = 'STOP_POLLING_JOKE';
export const FETCH_JOKE = 'FETCH_JOKE';
export const FETCH_JOKE_SUCCESS = 'FETCH_JOKE_SUCCESS';
export const FETCH_JOKE_FAILURE = 'FETCH_JOKE_FAILURE';
const defaultPollingInterval = 60000
function startPollingJoke(interval = defaultPollingInterval) {
return function (dispatch) {
const fetch = () => dispatch(fetchJoke())
dispatch({
type: START_POLLING_JOKE,
interval,
fetch,
})
}
}
function stopPollingJoke() {
return {
type: STOP_POLLING_JOKE
}
}
function fetchJoke() {
return {
type: FETCH_JOKE
};
}
function fetchJokeSuccess(data) {
return {
type: FETCH_JOKE_SUCCESS,
data
};
}
function fetchJokeFail(error) {
return {
type: FETCH_JOKE_FAILURE,
error
};
}
export function pollJokeCall(interval = defaultPollingInterval) {
return function (dispatch) {
dispatch(fetchJoke())
dispatch(startPollingJoke(interval))
}
}
export function fetchJokeCall() {
return function(dispatch){
dispatch(fetchJoke());
return axios.get('https://icanhazdadjoke.com', { headers: { 'Accept': 'application/json' }})
.then(function(result){
dispatch(fetchJokeSuccess(result.data))
})
.catch(error => dispatch(fetchJokeFail(error)));
}
}
Reducers
import {combineReducers} from 'redux';
import {
START_POLLING_JOKE,
STOP_POLLING_JOKE,
FETCH_JOKE,
FETCH_JOKE_SUCCESS,
FETCH_JOKE_FAILURE,
} from '../actions';
const defaultStateList = {
isFetching: false,
items:[],
error:{},
pollingId: null,
polling: false,
};
const joke = (state = defaultStateList, action) => {
switch (action.type){
case START_POLLING_JOKE:
clearInterval(state.pollingId)
return {
...state,
polling: true,
pollingId: setInterval(action.fetch, action.interval),
}
}
case STOP_POLLING_JOKE:
clearInterval(state.pollingId)
return {...state, polling: false, pollingId: null}
case FETCH_JOKE:
return {...state, isFetching:true};
case FETCH_JOKE_SUCCESS:
return {...state, isFetching:false, items:action.data};
case FETCH_JOKE_FAILURE:
return {...state, isFetching:false, error:action.data};
default:
return state;
}
};
const rootReducer = combineReducers({
joke
});
export default rootReducer;
Component (might have a bug because I'm not used to class components)
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { pollJokeCall, stopPollingJoke } from '../actions';
class Joke extends Component {
componentDidMount() {
this.props.pollJokeCall()
}
componentWillUnmount() {
this.props.stopPollingJoke()
}
render() {
return (
<div>
{this.props.joke.joke}
</div>
);
}
}
Joke.propTypes = {
pollJokeCall: PropTypes.func,
stopPollingJoke: PropTypes.func,
joke: PropTypes.array.isRequired,
};
function mapStateToProps(state) {
return {
joke: state.joke.items,
isfetching: state.joke.isFetching
};
}
export default connect(mapStateToProps, { pollJokeCall, stopPollingJoke })(Joke);
I have made a small (5kb gzipped) helper to create polling based on redux-thunk store. The idea is to have a logic to prevent registering the same polling twice, have callbacks between iterations and more.
https://www.npmjs.com/package/redux-polling-thunk
redux-saga is great and I've been using this with redux. It provides a great api to do things like delay, polling, throttling, race conditions, task cancellations. So using redux-saga, you can add a watcher whcih will keep on pooling
function* pollSagaWorker(action) {
while (true) {
try {
const { data } = yield call(() => axios({ url: ENDPOINT }));
yield put(getDataSuccessAction(data));
yield call(delay, 4000);
} catch (err) {
yield put(getDataFailureAction(err));
}
}
}
**Hello! my problem is my state is not uploading, is always empty altough my actions brings data correct. Can anyone give me some help of what am I doing wrong ?
I think is something with the name or the combine reducers part.
Maybe I am not accesing data correct with my reducer or something like that **
The object I receive from the api call has this format {categories: Array(4), items: Array(50)}
Component
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ItemList from '../components/ItemList/ItemList';
import { getItems } from './actions'
class ItemListContainer extends PureComponent {
async componentDidMount() {
const { getItems } = this.props;
await getItems()
console.log(this.props)
}
render() {
return <ItemList />;
}
}
const mapStateToProps = state => (
{
items: state.items.items,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
getItems,
},
dispatch,
);
export default connect(mapStateToProps, mapDispatchToProps)(ItemListContainer);
actions.js
export const GET_ITEMS = 'GET_ITEMS';
export const GET_ITEM = 'GET_ITEM';
export const GET_ITEM_DESCRIPTION = 'GET_ITEM_DESCRIPTION';
export function getItems(query) {
return async function (dispatch) {
// const res = await fetch(`http://localhost:3000/api/items?q=${query}`)
const res = await fetch(`http://localhost:3000/api/items?q=ipad`)
const items = await res.json()
return dispatch({
type: 'GET_ITEMS',
items: items.items,
})
}
}
reducer.js
import { GET_ITEMS } from './actions';
const initialState = {
items: [],
itemsLoaded: false,
};
export default function(state = initialState, action) {
const { type, data } = action;
switch (type) {
case GET_ITEMS:
return {
...state,
items: data,
itemsLoaded: true,
};
default: {
return {
...state
}
}
}
}
I was accessing { data} in the reducer which of course it was empty. The correnct action was items.
I'm trying to make an api call in action with axios and pass the results of it to the reducer.
Though action is triggered, reducer isn't. And I can't understand why.
Here's the component that should make api call before mounting
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
//actions
import { getPost } from '../actions/';
class PostShow extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
getPost(this.props.params.id);
}
render() {
console.log(this.props.activePost);
return (
<div>
<h1> hello from a post</h1>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
activePost: state.posts.activePost
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
getPost
}, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(PostShow);
Here's my action
import axios from 'axios';
import { FETCH_POSTS, SEND_POST, FETCH_POST } from './types';
const ROOT_URL = 'http://reduxblog.herokuapp.com/api';
const API_KEY = '?key=qwerty';
export function fetchPosts() {
const req = axios.get(`${ROOT_URL}/posts${API_KEY}`);
return {
type: FETCH_POSTS,
payload: req
}
}
export function sendPost(props) {
const req = axios.post(`${ROOT_URL}/posts${API_KEY}`, props);
return {
type: SEND_POST,
payload: req
}
}
export function getPost(id) {
console.log('action triggered');
const req = axios.get(`${ROOT_URL}/posts/${id}${API_KEY}`);
return {
type: FETCH_POST,
payload: req
}
}
And here's my reducer
import { FETCH_POSTS, FETCH_POST } from '../actions/types';
const INITIAL_STATE = {
allPosts: [],
activePost: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case FETCH_POSTS:
return {
...state,
allPosts: action.payload.data
};
case FETCH_POST:
console.log('reducer triggered');
return {
...state,
activePost: action.payload.data
};
default:
return state;
}
}
As a result I see 'action triggered' coming from console.log in action, and null coming from console.log in component, and no console.log from the reducer, so it's not triggered and I have no data to render in my component.
Though I make a request and get a response from server with the data, it doesn't go to the reducer. (moreover case FETCH_POSTS works fine and I can render a list of posts, but not a particular one).
"axios": "^0.17.0"
"redux-promise": "^0.5.3"
You need to use this.props.getPost in componentDidMount instead of getPost.
Connect sends bound action creator to component as a prop