I am pretty new to Next.JS and I was trying to set up Redux with my Next.JS application. Now my page is supposed to display a list of posts that I am calling in from an API. The page renders perfectly when I'm dispatching from useEffect() to populate the data on to my page, but getStaticProps() or getServerSideProps() are not working whatsoever!
Here is a bit of code that will give you a hint of what I've done so far:
store.js
import { useMemo } from 'react'
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
import rootReducer from './reducers/rootReducer'
const initialState = {}
const middlewares = [thunk]
let store
function initStore(preloadedState = initialState) {
return createStore(
rootReducer,
preloadedState,
composeWithDevTools(applyMiddleware(...middlewares))
)
}
export const initializeStore = (preloadedState) => {
let _store = store ?? initStore(preloadedState)
if (preloadedState && store) {
_store = initStore({
...store.getState(),
...preloadedState,
})
store = undefined
}
if (typeof window === 'undefined') return _store
if (!store) store = _store
return _store
}
export function useStore(initialState) {
const store = useMemo(() => initializeStore(initialState), [initialState])
return store
}
action.js
export const fetchPosts = () => async dispatch => {
const res = await axios.get('https://jsonplaceholder.typicode.com/posts')
dispatch({
type: FETCH_POSTS,
payload: res.data
})
}
_app.js
import { Provider } from 'react-redux'
import { createWrapper } from 'next-redux-wrapper'
import { useStore } from '../redux/store'
export default function MyApp({ Component, pageProps }) {
const store = useStore(pageProps.initialReduxState)
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
These are the files that I needed for the basic redux setup. Once my store was set up and I wrapped my app around the Provider, I initially though of using useEffect() hook to populate data on a component that was rendering inside my index.js file.
component.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchPosts } from '../redux/actions/postsAction'
const Posts = () => {
const dispatch = useDispatch()
const { items } = useSelector(state => state.posts)
useEffect(() => {
dispatch(fetchPosts())
}, [])
return (
<div className="">
<h1>Posts</h1>
{items.map(post => {
return (<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>)
})}
</div>
)
}
export default Posts
This worked perfectly! All my posts were showing up inside the component. The problem occurred when I was trying to achieve the same behaviour with server side rendering (or even SSG). I wanted to populate the data during the pre-render phase but for some reason the items array which is supposed to hold all the data is empty, basically meaning that the disptacher was never called! Here is the piece of code that is bothering me (exactly same as previous code, but this time I'm using getStaticProps() instead of useEffect()):
component.js
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchPosts } from '../redux/actions/postsAction'
const Posts = ({ items }) => {
return (
<div className="">
<h1>Posts</h1>
{items.map(post => {
return (<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>)
})}
</div>
)
}
export async function getStaticProps() {
console.log('Props called')
const dispatch = useDispatch()
const { items } = useSelector(state => state.posts)
dispatch(fetchPosts())
console.log(items)
return { props: { items } }
}
export default Posts
By running this, I'm getting an error that items is empty! Please help me, I have no clue what's going wrong here.
Well I fixed this issue myself but I forgot to post an answer for it, my bad!
The problem here really is very simple, hooks don't work outside of a functional component!
I think, inside of getStaticProps just call API or get datas from DB and returns it as props to pages/index.js (any component you want) and inside of this component we can get datas from getStaticProps as props.
Also we can set it as global state using useDispatch of react-redux. After that any component we can call those states using redux mapStateToProps. This is my solution.
This maybe a solution if anyone faced this problem,
import React from 'react';
import {useSelector} from 'react-redux';
import {wrapper} from '../store';
export const getStaticProps = wrapper.getStaticProps(store => ({preview})
=> {
console.log('2. Page.getStaticProps uses the store to dispatch things');
store.dispatch({
type: 'TICK',
payload: 'was set in other page ' + preview,
});
});
// you can also use `connect()` instead of hooks
const Page = () => {
const {tick} = useSelector(state => state);
return <div>{tick}</div>;
};
export default Page;
Got it from here: https://github.com/kirill-konshin/next-redux-wrapper
Related
I am trying to implement Redux in a Next.js app and have problems getting the dispatch function to work in getInitialProps. The store is returned as undefined for some reason that I cannot figure out. I am using next-redux-wrapper. I have followed the documentation on next-redux-wrapper GitHub page but somewhere on the way it goes wrong. I know the code is working - I used axios to directly fetch the artPieces and then it worked just fine but I want to use Redux instead. I am changing an react/express.js app to a Next.js app where I will use the API for the basic server operations needed. This is just a small blog app.
Here is my store.js:
import { createStore } from 'redux';
import { createWrapper, HYDRATE } from 'next-redux-wrapper';
// create your reducer
const reducer = (state = { tick: 'init' }, action) => {
switch (action.type) {
case HYDRATE:
return { ...state, ...action.payload };
case 'TICK':
return { ...state, tick: action.payload };
default:
return state;
}
};
// create a makeStore function
const makeStore = (context) => createStore(reducer);
// export an assembled wrapper
export const wrapper = createWrapper(makeStore, { debug: true });
And here is the _app.js:
import './styles/globals.css';
import { wrapper } from '../store';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default wrapper.withRedux(MyApp);
And finally here is where it does not work. Trying to call dispatch on the context to a sub component to _app.js:
import React from 'react';
import { ArtPiecesContainer } from './../components/ArtPiecesContainer';
import { useDispatch } from 'react-redux';
import axios from 'axios';
import { getArtPieces } from '../reducers';
const Art = ({ data, error }) => {
return (
<>
<ArtPiecesContainer artPieces={data} />
</>
);
};
export default Art;
Art.getInitialProps = async ({ ctx }) => {
await ctx.dispatch(getArtPieces());
console.log('DATA FROM GETARTPIECES', data);
return { data: ctx.getState() };
};
This should probably work with "next-redux-wrapper": "^7.0.5"
_app.js
import { wrapper } from '../store'
import React from 'react';
import App from 'next/app';
class MyApp extends App {
static getInitialProps = wrapper.getInitialAppProps(store => async ({Component, ctx}) => {
return {
pageProps: {
// Call page-level getInitialProps
// DON'T FORGET TO PROVIDE STORE TO PAGE
...(Component.getInitialProps ? await Component.getInitialProps({...ctx, store}) : {}),
// Some custom thing for all pages
pathname: ctx.pathname,
},
};
});
render() {
const {Component, pageProps} = this.props;
return (
<Component {...pageProps} />
);
}
}
export default wrapper.withRedux(MyApp);
and Index.js
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { END } from 'redux-saga'
import { wrapper } from '../store'
import { loadData, startClock, tickClock } from '../actions'
import Page from '../components/page'
const Index = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(startClock())
}, [dispatch])
return <Page title="Index Page" linkTo="/other" NavigateTo="Other Page" />
}
Index.getInitialProps = wrapper.getInitialPageProps(store => async (props) => {
store.dispatch(tickClock(false))
if (!store.getState().placeholderData) {
store.dispatch(loadData())
store.dispatch(END)
}
await store.sagaTask.toPromise()
});
export default Index
For the rest of the code you can refer to nextjs/examples/with-redux-saga, but now that I'm posting this answer they're using the older version on next-redux-wrapper ( version 6 ).
I am learning rematch/redux. I can't get the state to show with the API.
I have the model imported in index.js along with the store and the provider. These are my reducers/effects:
import { getItems } from './service'
const products = {
state: {
products: [],
},
reducers: {
setProducts(state, products) {
return {
...state,
products,
};
},
},
effects: {
async loadProducts() {
const products = await getItems() // <-- This is the api working normally
this.setProducts(products)
},
}
}
export default products
And this is my component:
import './App.css';
import { connect } from 'react-redux';
import React, { useEffect } from 'react';
const mapStateToProps = ({ products }) => {
return {
...products
}
}
const mapDispatchToProps = ({ products }) => {
return {
...products
}
}
const App = ({ products }) => {
useEffect(() => {
console.log(products)
})
return (
<div className="App">
{console.log(products)}
</div>
)
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
I am not sure what I am missing.
Thank you.
I'm Rematch maintainer, you should review our documentation or consider buying the official Redux made easy with Rematch book where you'll learn all this questions.
I highly recommend using React-Redux hooks instead of connect method.
import './App.css';
import { useSelector, useDispatch } from 'react-redux';
import React, { useEffect } from 'react';
const App = () => {
const dispatch = useDispatch();
const { products } = useSelector(rootState => rootState.products)
useEffect(() => {
dispatch.products.loadProducts();
}, []);
return (
<div className="App">
{console.log(products)}
</div>
)
}
export default App;
Be careful with hooks, you're forgetting to add the deps array to useEffect, any code you add there will run infinitely
Your Rematch models looks fine, you just need to work more on React essentials :)
Redux rematch
Create music listing app using youtube api
How to create folder structure and setup store for react redux
Working code sandbox link
https://codesandbox.io/s/rematch-yf77l0?file=/src/pages/musics/index.jsx
Considering the following project setup on a react-redux application that uses context API to avoid prop drilling. The example given is simplified.
Project Setup
React project uses React Redux
Uses context API to avoid prop drilling in certain cases.
Redux store has a prop posts which contains list of posts
An action creator deletePost(), which deletes a certain post by post id.
To avoid prop drilling, both posts and deletePosts() is added to a context AppContext and returned by a hook funciton useApp().
posts array is passed via contexts so it is not used by connect() function. Important
Problem:
When action is dispatched store is updated however Component is not re-rendered (because the prop is not connected?). Of course, if I pass the prop with connect function and drill it down to child rendering works fine.
What is the solution?
Example Project
The example project can be found in codesandbox. Open up the console and try to click the delete button. You will see no change in the UI while you can see the state is updated in the console.
Codes
App.js
import Home from "./routes/Home";
import "./styles.css";
import { AppProvider } from "./context";
export default function App() {
return (
<AppProvider>
<div className="App">
<Home />
</div>
</AppProvider>
);
}
context.js
import { useDispatch, useStore } from "react-redux";
import { useContext, createContext } from "react";
import { deletePost } from "./redux/actions/posts";
export const AppContext = createContext();
export const useApp = () => {
return useContext(AppContext);
};
export const AppProvider = ({ children }) => {
const dispatch = useDispatch();
const {
posts: { items: posts }
} = useStore().getState();
const value = {
// props
posts,
// actions
deletePost,
dispatch
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
Home.js
import { connect } from "react-redux";
import Post from "../components/Post";
import { useApp } from "../context";
const Home = () => {
const { posts } = useApp();
return (
<section>
{posts.map((p) => (
<Post key={p.id} {...p} />
))}
</section>
);
};
/*
const mapProps = ({ posts: { items: posts } }) => {
return {
posts
};
};
*/
export default connect()(Home);
Post.js
import { useApp } from "../context";
const Post = ({ title, content, id }) => {
const { deletePost, dispatch } = useApp();
const onDeleteClick = () => {
console.log("delete it", id);
dispatch(deletePost(id));
};
return (
<article>
<h1>{title}</h1>
<p>{content}</p>
<div className="toolbar">
<button onClick={onDeleteClick}>Delete</button>
</div>
</article>
);
};
export default Post;
You're not using the connect higher order component method properly . Try using it like this so your component will get the states and the function of your redux store :
import React from 'react';
import { connect } from 'react-redux';
import { callAction } from '../redux/actions.js';
const Home = (props) => {
return (
<div> {JSON.stringify(props)} </div>
)
}
const mapState = (state) => {
name : state.name // name is in intialState
}
const mapDispatch = (dispatch) => {
callAction : () => dispatch(callAction()) // callAction is a redux action
//and should be imported in the component also
}
export default connect(mapState , mapDispatch)(Home);
You can access the states and the actions from your redux store via component props.
Use useSelector() instead of useState(). Example codepen is fixed.
Change from:
const { posts: { items: posts } } = useStore().getState();
Change to:
const posts = useSelector(state => state.posts.items);
useStore() value is only received when component is first mounted. While useSlector() will get value when value is changed.
I cant for the life of my get one of my actions to call the reducer.
I've written multiple other actions and reducers in this app, which works perfectly.
It is the beginning of a filter function. I have a text input field, where I constantly keep track of the input field state, and dispatch the field value to the redux action. I have a console.log inside the action, which logs every keypress properly.
What I cant seem to understand, is why the reducer isn't called at each keypress. I've tried multiple console.log's inside the reducer, however none of them gets logged with keypresses.
The first console.log inside the reducer is called when I refresh the page.
if I try to log action.type instead, I get:
##redux/PROBE_UNKNOWN_ACTION1.0.i.0.0.9
If I try the same in any of the other reducers I've written in the same app, I get the appropriate type logged out.
Some code:
Filter Component:
import React, { useState } from 'react'
import { useDispatch } from 'react-redux';
import { filterAnecdotes } from '../reducers/filterReducer';
const Filter = () => {
const [value, setValue] = useState("");
const handleChange = (e) => {
setValue(e.target.value)
}
useDispatch(filterAnecdotes(value));
const style = {
marginBottom: 10
}
return (
<div style={style}>
filter <input onChange={handleChange} />
</div>
)
}
export default Filter
Reducer and action:
Here, I haven't figured out how to get the state of all anecdotes, and what to actually return. For now, I'm just trying to have it get called properly.
const filterReducer = (state = null, action) => {
// These logs only get logged on refresh.
// The action.type should be 'SEARCH', but is not.
console.log("From filterReducer")
console.log(action.type)
switch(action.type) {
case 'SEARCH':
// This is not called at all.
console.log(action.type, action.data)
return action.data;
default:
return state
}
}
export const filterAnecdotes = (filter) => {
console.log(filter);
return {
type: 'SEARCH',
data: filter
}
}
export default filterReducer;
Example of a redux file that actually works:
const reducer = (state = [], action) => {
console.log(state, action)
switch(action.type){
case 'NEW_ENTRY_NOTIFICATION':
console.log(action.type)
return [...state, action.data]
case 'NEW_VOTE_NOTIFICATION':
return [...state, action.data]
case 'HIDE_NOTIFICATION':
return []
default:
return state
}
}
export const createNewEntryNotification = (notification) => {
return {
type: 'NEW_ENTRY_NOTIFICATION',
data: notification
}
}
export const createNewVoteNotification = (notification) => {
return {
type: 'NEW_VOTE_NOTIFICATION',
data: notification
}
}
export const hideNotification = () => {
return {
type: 'HIDE_NOTIFICATION'
}
}
export default reducer
App component (should be irrelevant)
import React from 'react';
import NewEntry from './components/AnecdoteForm'
import AnecdoteList from './components/AnecdoteList'
import Notification from './components/Notification'
import Filter from './components/Filter';
const App = () => {
return (
<div>
<h2>Anecdotes</h2>
<Filter />
<Notification />
<AnecdoteList />
<NewEntry />
</div>
)
}
store (should be irrelevant)
import anecdoteReducer from './anecdoteReducer';
import notificationReducer from './notificationReducer';
import filterReducer from './filterReducer';
import { combineReducers } from 'redux'
const reducer = combineReducers({
anecdotes: anecdoteReducer,
notifications: notificationReducer,
filters: filterReducer,
});
export default reducer
index.js (should also be irrelevant)
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './App'
import reducer from './reducers/store'
const store = createStore(reducer)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
export default App
I'll be happy to provide more information if needed.
This is for a project from fullstackopen.
Try to instantiate useDispatch like this after const [value, setValue] = useState("");
const dispatch = useDispatch();
And then use the intance to dispatch the action
dispatch(filterAnecdotes(value));
the use of useDispatch is misunderstood.
Link for reference: https://react-redux.js.org/api/hooks#usedispatch
You should create a dispatch from useDispatch:
const dispatch = useDispatch();
dispatch(filterAnecdotes(value));
I am trying to make a blog-style application. I am running a django server and trying to make a react-redux frontend. I am using redux-devtools and when I comment out the error code, redux seems to have the data in the state. Not sure what is going wrong. Also, I am using redux-thunk and axios to communicate with the backend. I am pretty much copying from a youtube tutorial.
This is the reducer reducers/posts.js
import {GET_POSTS} from "../actions/types";
const initialState = {
posts: []
}
export default function(state = initialState, action) {
switch (action.type) {
case GET_POSTS:
return {
...state,
posts: action.payload
}
default:
return state;
}
}
this is the action actions/posts.js
import axios from "axios";
import {GET_POSTS} from "./types";
export const getPosts = () => dispatch => {
axios.get('/get/posts/').then(res => {
dispatch({
type: GET_POSTS,
payload: res.data
})
}).catch(error => {console.log(error)})
}
this is reducers/index.js
import {combineReducers} from 'redux';
import posts from "./posts";
export default combineReducers({
posts
});
this is 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 store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
this is components/Home.js (ERROR HERE)
import React from 'react';
import {connect} from 'react-redux';
import {getPosts} from '../actions/posts';
class Home extends React.Component {
componentDidMount() {
this.props.getPosts();
}
render() {
console.log(this.props.posts); //undefined
const posts = this.props.posts.map(post => (
<div className="post">
<h1>{post.title}</h1>
<p>{post.message}</p>
</div>
)
)
return (
{posts}
)
}
}
const mapStateToProps = state => ({
posts: state.posts // I have tried state.posts
// and state.posts.posts. I think
//state.posts might be undefined which is
//causing this.props.posts to be undefined in the component
});
export default connect(mapStateToProps, {getPosts})(Home);
ANSWER: I found out that render is called multiple times and the first time it is called, this.props.posts is undefined, so I put an if statement for if this.props.posts is undefined and if it is not, I render the list.
On your return inside Home component do this:
return (
{this.posts.length > 0 && posts}
)
Your initial render, is trying to render an array of elements that are not being yet fetched from the server. All fetch calls to web apis and to backend servers are async which means they will execute as soon as the stack frame of the JS engine is free. Adding the conditional statement will allow you to do an extra checkwhich will rerender the page since the data will be fetched and stored to your reducer. Additionally, when not sure what is the state/shape of your redux store, you can always console log it like this:
const mapStateToProps = state => {
console.log(state);
return {
posts: state.posts
}
}