I'm trying to implement a simple test case for using Redux-thunks with Next JS but keep getting the error
Error: Circular structure in "getInitialProps" result of page "/".
https://err.sh/zeit/next.js/circular-structure
I have gotten this all to work once before, and am sure I'm making some obvious error.
I'd appreciate any help you could provide. I've been poking at this for an hour and I'm not seeing where I'm going wrong...
I've traced it down to the dispatch within my thunk, that is dispatch(getItemsSuccess(data)) in the following code in action-creators.js. That is, if I remove that dispatch, I don't get the error.
// action-creators.js
import {GET_ITEMS_SUCCESS} from "./action-types"
import axios from 'axios'
export const getItemsSuccess = (data) => ({ type: GET_ITEMS_SUCCESS, data });
export const getItems = () => async (dispatch,getState) => {
try {
const data = await axios.get(`https://api.themoviedb.org/3/genre/movie/list?api_key=12345xyz`)
return dispatch(getItemsSuccess(data))
} catch(e) {
console.log(`error in dispatch in action-creators: ${e}`)
}
}
My _app.js is
import React from 'react'
import {Provider} from 'react-redux'
import App, {Container} from 'next/app'
import withRedux from 'next-redux-wrapper'
import configureStore from '../redux/configure-store'
class MyApp extends App {
static async getInitialProps({Component, ctx}) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return {pageProps}
}
render() {
const {Component, pageProps, store} = this.props
return (
<Container>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</Container>
)
}
}
export default withRedux(configureStore, { debug: true })(MyApp);
and my index.js is
import React, {Component} from 'react'
import {connect} from 'react-redux'
import {getItems} from "../redux/action-creators"
class Index extends Component {
static async getInitialProps({store}) {
try {
await store.dispatch(getItems())
} catch(e) {
console.log(`error in dispatch in index.js: ${e.message}`)
}
}
render() {
return <div>Sample App</div>
}
}
export default connect(state => state)(Index)
and finally I configure the store thus
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './root-reducer';
const bindMiddleware = middleware => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension');
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
function configureStore(initialState = {}) {
const store = createStore(
rootReducer,
initialState,
bindMiddleware([thunk]),
);
return store;
}
export default configureStore;
Again, any help much appreciated -- I have been going over this for some time and am not seeing the missing piece...
When you return data from axios, one has to access the data within the data, to wit, instead of
const data = await
axios.get(`https://api.themoviedb.org/3/genre/movie/list?api_key=12345xyz`)
return dispatch(getItemsSuccess(data))
I should have written
axios.get(`https://api.themoviedb.org/3/genre/movie/list?api_key=12345xyz`)
return dispatch(getItemsSuccess(data.data))
Why This Error Occurred
getInitialProps is serialised to JSON using JSON.stringify and sent to the client side for hydrating the page.
However, the result returned from getInitialProps can't be serialised when it has a circular structure.
Possible Ways to Fix It
Circular structures are not supported, so the way to fix this error is removing the circular structure from the object that is returned from getInitialProps. In your case you just need to extract appropriate data like #Cerulean explained.
Related
I am using Redux and Redux saga with Nextjs, i wrapped the store on _app.js file and when i call the api with post or get requests Redux-Saga is getting called at least two times, specially for post requests for example if i want to register a user using the api it is registering the user two times on the database
PS: I am using rootSaga and i am not calling a saga twice there
This is my store file:
import { createStore, applyMiddleware, compose } from "redux";
import createSagaMiddleware from "redux-saga";
import reducers from "./reducers";
import sagas from "./sagas";
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
export function configureStore(initialState) {
const store = createStore(
reducers,
initialState,
compose(applyMiddleware(...middlewares))
);
sagaMiddleware.run(sagas);
if (module.hot) {
module.hot.accept("./reducers", () => {
const nextRootReducer = require("./reducers");
store.replaceReducer(nextRootReducer);
});
}
return store;
}
export const wrapper = createWrapper(configureStore, { debug: true });
And this is my _app.js file
import "../styles/styles.global.scss";
import "../styles/Home.module.scss";
import React from "react";
import App, { Container } from "next/app";
import { Provider, connect } from "react-redux";
import withRedux from "next-redux-wrapper";
import { configureStore, wrapper } from "../libs/store";
const context = React.createContext(undefined);
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return { ...pageProps };
}
render() {
const { Component, pageProps, router } = this.props;
return (
<Provider store={configureStore()} context={context}>
<Component {...pageProps} key={router.asPath} />
</Provider>
);
}
}
export default wrapper.withRedux(MyApp);
Thank you.
I Fixed it by removing the provider from _app.js and deleting _document.js
PS: This solutions is for Nextjs >= 10.0.0
I am trying to use React with Redux for the frontend part with django rest framework in the backend. Got the issue getState in Provider tag in App component because of issue in store. And when i try to use the map function in the Words.js, I get error of undefined use of map. And I believe this is because of value of the array is null. Hence to fixed this error of getState.
Got this error even on including the store in Provider of App component when a reducers was not defined.
When I load a static array it does get rendered properly in the specific component.
This is Redux Store in the filename:store.js
import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import rootReducer from './reducers'
const initialState = {};
const middleware = [thunk];
const enhancer = composeWithDevTools(applyMiddleware(...middleware));
const store = createStore(
rootReducer,
initialState,
enhancer
);
export default store;
The index.js file is below
import App from './components/App'
import ReactDOM from 'react-dom';
import React from 'react';
ReactDOM.render(<App />, document.getElementById("app"));
They action types file types.js using django rest_framework to create the data.
export const GET_WORDS = "GET_WORDS";
The action file words.js
import { GET_WORDS } from "./types";
import axios from 'axios';
export const getWords = () => dispatch => {
axios.get('/api/words/')
.then(res => {
dispatch({
type: GET_WORDS,
payload: res.data
});
}).catch(err => console.log(err));
}
combined reducer file
import { combineReducers } from "redux";
import words from './words';
export default combineReducers({
words
});
The reducer file word.js
import { GET_WORDS } from '../actions/types';[enter image description here][1]
const initialState = {
words: []
}
export default function (state = initialState, action) {
switch (action.type) {
case GET_WORDS:
return {
...state,
words: action.payload
}
default:
return state;
}
}
The Component in which the words list will be called: Words.js
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getWords } from "../../../actions/words";
export class Words extends Component {
static propTypes = {
words: PropTypes.array.isRequired,
getWords: PropTypes.func.isRequired
};
componentDidMount() {
this.props.getWords();
}
render() {
return (
<Fragment>
Hi
</Fragment>
)
}
}
const mapStateToProps = state => ({
words: state.words.words
});
export default connect(mapStateToProps, { getWords })(Words);
And finally the App component
import React, { Component, Fragment } from 'react';
import Footer from './Layout/Footer/Footer';
import Header from './Layout/Header/Header';
import WordsDashboard from './Content/Words/WordsDashboard';
import { store } from '../store';
import { Provider } from "react-redux";
import { Words } from './Content/Words/Words';
export class App extends Component {
render() {
return (
<Provider store={store}>
<Fragment>
<Header />
React Buddy
<Words />
<Footer />
</Fragment>
</Provider>
)
}
}
export default App;
Your initialState has only words prop, so when mapping it to props you have one extra words level. Try changing it to:
const mapStateToProps = state => ({
words: state.words
});
Also you need to use mapDispatchToProps for getWords, since in your current code you're missing dispatch wrapper:
const mapDispatchToProps = dispatch => ({
getWords: () => dispatch(getWords())
})
export default connect(mapStateToProps, mapDispatchToProps)(Words);
In React Single Page App, we need to separate the logic of createStore to another component (usually called <Root />) to reuse it in your test file to let connect function link with the store
Root.js
import React from "react";
import { Provider } from "react-redux";
import { createStore } from "redux";
import reducers from "reducers";
import { applyMiddleware } from "redux";
import reduxPromise from "redux-promise";
const appliedMiddlewares = applyMiddleware(reduxPromise);
export default ({ children, initialState = {} }) => {
const store = createStore(reducers, initialState, appliedMiddlewares);
return <Provider store={store}>{children}</Provider>;
};
And then in your test file, to mount or shallow your component, your code should look like this:
import Root from "Root";
let mounted;
beforeEach(() => {
mounted = mount(
<Root>
<CommentBox />
</Root>
);
});
But for the case of Next.JS, the logic to let redux works with it was implemented in _app.js file, with some wrapper components (<Container>, <Component> ) that I do not know how it works so I could not find the way to separate the createStore logic
_app.js
import App, { Container } from "next/app";
import React from "react";
import Root from '../Root';
import withReduxStore from "../lib/with-redux-store";
import { Provider } from "react-redux";
class MyApp extends App {
render() {
const { Component, pageProps, reduxStore } = this.props;
return (
<Container>
<Provider store={reduxStore}>
<Component {...pageProps} />
</Provider>
</Container>
);
}
}
export default withReduxStore(MyApp);
Anyone knows it ? Many many thanks for helping me solve this.
Possibly I'm late adding a response, but this is what I did and worked!
First I imported the custom app:
import App from "../_app";
import configureStore from "redux-mock-store";
import thunk from "redux-thunk";
import { state } from "../../__mocks__/data";
const middlewares = [thunk];
const mockStore = configureStore(middlewares)(state);
Then I mocked the getInitialProps from the _app.js like:
const context = {
store: mockStore,
req: {
headers: {
cookie: ""
}
}
};
const props = await App.getInitialProps({
ctx: context,
Component: {
getInitialProps: jest.fn(() => {
return Promise.resolve({ ... });
})
}
});
Then, debugging over node_modules\next-redux-wrapper\src\index.tsx I noticed how the initialState must be set.
Then I added the following code:
delete window.__NEXT_REDUX_STORE__;
const wrapper = shallow(<App {...props} initialState={state}/>);
expect(toJson(wrapper)).toMatchSnapshot();
And run the tests, everything now works as expected.
If there is a cleaner solution please let me know.
I hope It works for you!
So I've recently started learning Redux and now I'm trying to make my first app with it, but I've stumbled upon a problem which I cannot resolve on my own. Basically I want a user to click a button (there will be authentication) and fetch all his or hers data from Firebase and display it.
Here is my index.js:
// Dependencies
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import createHistory from 'history/createBrowserHistory';
import { ConnectedRouter, routerReducer, routerMiddleware } from 'react-router-redux';
import ReduxPromise from "redux-promise";
import ReduxThunk from 'redux-thunk';
// Reducers
import rootReducer from './reducers';
// ServiceWorker
import registerServiceWorker from './registerServiceWorker.js';
// Styles
import './styles/index.css';
// Components
import App from './containers/App.js';
const history = createHistory();
const middleware = routerMiddleware(history);
// Create store
const store = createStore(
combineReducers({
...rootReducer,
router: routerReducer
}),
applyMiddleware(ReduxThunk, middleware, ReduxPromise)
)
ReactDOM.render((
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>
), document.getElementById('root'));
registerServiceWorker();
And my main container, App.js:
import React, { Component } from 'react';
import { Route, Switch, withRouter } from 'react-router-dom'
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import firebase from 'firebase';
import firebaseConfig from '../firebaseConfig.js';
// Actions
import { fetchAllMonths } from "../actions/index";
// Static components
import Nav from '../components/Nav.js';
// Routes
import CurrentMonth from '../components/CurrentMonth.js';
import AddNewMonth from '../components/AddNewMonth.js';
import Archive from '../components/Archive.js';
import Settings from '../components/Settings.js';
class App extends Component {
constructor (props) {
super(props);
this.login = this.login.bind(this);
}
componentWillMount() {
firebase.initializeApp(firebaseConfig);
firebase.auth().signInAnonymously().catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
});
}
login() {
this.props.fetchAllMonths();
}
render() {
if (this.props.data === undefined) {
return (
<button onClick={this.login}>Login</button>
)
} else if (this.props.data !== undefined) {
return (
<main className="main-container">
<Nav user="user"/>
<Switch>
<Route exact path='/' component={CurrentMonth}/>
<Route path='/aktualny' component={CurrentMonth}/>
<Route path='/nowy' component={AddNewMonth}/>
<Route path='/archiwum' component={Archive}/>
<Route path='/ustawienia' component={Settings}/>
</Switch>
</main>
);
}
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ fetchAllMonths }, dispatch);
}
function mapStateToProps({ data }) {
return { data };
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App))
Main action, fetchAllMonths:
// import firebase from 'firebase';
// Firebase Config
// import axios from 'axios';
export const FETCH_ALL_MONTHS = 'FETCH_ALL_MONTHS';
export function fetchAllMonths() {
/*const database = firebase.database();
const data = database.ref('/users/grhu').on('value', function(snapshot) {
return snapshot.val();
});
console.log(data) */
const data = fetch('https://my-finances-app-ef6dc.firebaseio.com/users/grhu.json')
.then(async (response) => response.json())
.then(data => {
console.log(data);
return data;
}
)
console.log(data);
return {
type: FETCH_ALL_MONTHS,
payload: data
};
};
Reducers index.js:
import { combineReducers } from "redux";
import data from "./reducer_load_from_db";
const rootReducer = combineReducers({
data: data
});
export default rootReducer;
And finally my reducer:
import { FETCH_ALL_MONTHS } from "../actions/index";
export default function(state = [], action) {
switch (action.type) {
case FETCH_ALL_MONTHS:
return [action.payload.data, ...state];
default:
return state;
}
return state;
}
So I'm sure that fetch works fine, because console.log(data) gives me a valid JSON file, but second console.log(data) with the passed const gives me a promise, which then I send as a payload to a Reducer. CreateStore also seems to work, because in the React dev console I can see a "data" prop in App container. I use redux-promise which should resolve the Promise in payload and return a JSON to the store, but data remains undefined. Also tried redux-promise-middleware, but again, no luck.
What am I missing? I've looked at that code for several hours and I cannot understand what is wrong with it.
I'll appreciate all the answers, but i really want to understand the issue, not just copy-paste a solution.
Thanks in advance!
Initial Answer
From what I'm reading in your fetchAllMonths() action creator, you're setting a property on the action it returns called payload equal to the data returned from your API call.
return {
type: FETCH_ALL_MONTHS,
payload: data
};
If you logged action.payload in your reducer like so...
switch (action.type) {
case FETCH_ALL_MONTHS:
console.log(action.payload)
return [action.payload.data, ...state];
default:
return state;
}
I believe you'd see the data returned from your API call.
So then in your reducer you would be expecting action.payload as a property of the FETCH_ALL_MONTHS action. And you'd want to use the spread operator ...action.payload.
Also, to make your logic a little easier to read, why not try using an async action to fetch data and then dispatch an action that takes in the data returned from the API call?
Hope that helps!
Updated Answer
As I thought about this and read your reply to my answer, I think you may need to use an async action to ensure your data was successfully fetched. I made a really simple CodePen example using async actions.
https://codepen.io/joehdodd/pen/rJRbZG?editors=0010
Let me know if that helps and if you get it working that way.
I connected redux to Next.js app just like in the docs (not sure what mapDispatchToProps does in the example though):
Init store method:
import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import axios from 'axios';
import axiosMiddleware from 'redux-axios-middleware';
import tokenMiddleware from './tokenMiddleware';
import getReducer from './combineReducers';
const logger = createLogger({ collapsed: true, diff: true });
const axiosMw = axiosMiddleware(axios.create(), { successSuffix: '_SUCCESS', errorSuffix: '_FAILURE' });
export default function initStore(logActions) {
return function init() {
const middleware = [tokenMiddleware, axiosMw];
if (logActions) middleware.push(logger);
return createStore(getReducer(), applyMiddleware(...middleware));
};
}
HOC which I use to connect pages:
import 'isomorphic-fetch';
import React from 'react';
import withRedux from 'next-redux-wrapper';
import { setUser } from 'lib/publisher/redux/actions/userActions';
import PublisherApp from './PublisherApp';
import initStore from '../redux/initStore';
export default Component => withRedux(initStore(), state => ({ state }))(
class extends React.Component {
static async getInitialProps({ store, isServer, req }) {
const cookies = req ? req.cookies : null;
if (cookies && cookies.user) {
store.dispatch(setUser(cookies.user));
}
return { isServer };
}
render() {
console.log(this.props.state);
return (
<PublisherApp {...this.props}>
<Component {...this.props} />
</PublisherApp>
);
}
}
);
The problem I'm having is that dispatched action
store.dispatch(setUser(cookies.user));
seems to work fine on server (I've debugged reducer and I know this user object from cookies is indeed handled by reducer) but when I do console.log(this.props.state) I get reducer with initial state - without user data.
You are missing second parameter inside createStore call. Try this:
export default function initStore(logActions) {
return function init(initData) {
const middleware = [tokenMiddleware, axiosMw];
if (logActions) middleware.push(logger);
return createStore(getReducer(), initData, applyMiddleware(...middleware));
};
}
Notice added initData parameter and it's usage.