I'm getting TypeError: Cannot read property 'map' of undefined in Products because it is being rendered before the products in store is updated. Therefore I'm mapping an empty array.
My question is - how can I wait for async action (fetchProducts) to finish before rendering Products component?
How fetchProducts work:
In App, fetchProducts grab data from the api; meanwhile state.status is "".
It dispatch(fetchProductsRequest()) and changes state.status to "loading".
Finally if fetching of products is successful, it dispatch(fetchProductsSuccess()) and changes state.status to "success".
Products.js
import React, { Component } from 'react';
import Product from './Product';
const Products = ({ products, addToCart }) => {
console.log(products);
console.log(addToCart);
return (
<div className='img-container'>
{products.map((product, index) => {
let num = index + 1;
return (
<Product key={product.name} image={product.image} name={product.name}
price={product.price} num={num} addToCart={addToCart} />
);
})}
</div>
);
};
ProductsContainer.js
import React from 'react';
import { connect } from 'react-redux';
import AddToCart from '../actions/AddToCart';
import Products from '../components/Products';
const ProductsContainer = ({ products, addToCart }) => {
console.log(products);
return (
<Products products={products} addToCart={addToCart} />
);
}
const mapStateToProps = state => ({
products: state.products
});
const mapDispatchToProps = dispatch => ({
AddToCart: (num, cartItem) => dispatch(AddToCart(num, cartItem))
});
export default connect(mapStateToProps, mapDispatchToProps)(ProductsContainer);
FetchProducts.js
import FetchProductsRequest from './FetchProductsRequest';
import FetchProductsSuccess from './FetchProductsSuccess';
const FetchProducts = () => {
return (dispatch) => {
fetch('/products')
.then(res => res.json())
.then(res => dispatch(FetchProductsSuccess(res)))
.catch(err => console.log(err));
dispatch(FetchProductsRequest());
}
}
export default FetchProducts;
Make ProductsContainer a class component, not functional component
In ProductsContainer in componentDidMount call action.
In render method of the ProductsContainer do something like this.
render(){
if (!this.state.products){
return <div>Loading</div>
}
return (
<Products products={this.state.products} />
);
}
Related
I just updated my next-redux-wrapper package and did some changes according to their latest doc. However, the component didn't receive the props returned from MyComponent.getInitialProps = wrapper.getInitialPageProps(store => (context) => ({ foo: "bar }))
When I console the props on MyComponent, there is no "foo"
Has anyone solved this or know why this happens?
I tried to change my __app.js and use getInitialProps without wrapper.getInitialPageProps, but it changes nothing. Here is my __app.js
import { wrapper } from 'store';
import { Provider } from 'react-redux';
import App from 'next/app';
const MyApp = ({ Component, ...rest }) => {
const { store, props } = wrapper.useWrappedStore(rest);
return (
<Provider store={store}>
<Component {...props.pageProps} />
</Provider>
);
};
MyApp.getInitialProps = wrapper.getInitialAppProps(
(store) => async (appCtx) => {
const appProps = await App.getInitialProps(appCtx);
return {
pageProps: {
...appProps.pageProps,
},
};
}
);
export default MyApp;
Problem: My next.js app crash on client side because of empty store object, but if I try to read this object in getServerSideProps it`s ok.
I have 2 pages in my app, profile/[id] and post/[id], all of them have getServerSideProps
User flow:
User coming on profile/[id] by friend`s link
On profile/[id] page he has profile data and 3x3 posts grid, every post is a link to post/[id]
Click on post
Navigate to post/[id] - here he has some post data: username, image, createdAt etc...
Expected: Server render html for post page after successful request
Received: Client crash after trying to read field of empty object
Question: Can you tell my what's wrong with my code? I have HYDRATE for postSlice and default next-redux-wrapper code so I'm confused.
Code:
store.ts
import {configureStore} from "#reduxjs/toolkit";
import {createWrapper} from "next-redux-wrapper";
import profileSlice from './reducers/profileSlice';
import postsSlice from './reducers/postsSlice';
import postSlice from './reducers/postSlice';
export const makeStore = () =>
configureStore({
reducer: {
profile: profileSlice,
posts: postsSlice,
post: postSlice
},
devTools: true
});
export type Store = ReturnType<typeof makeStore>;
export type RootState = ReturnType<Store['getState']>;
export const wrapper = createWrapper<Store>(makeStore);
_app.tsx
//
...imports
//
function App({Component, ...rest}: AppProps) {
const {store, props} = wrapper.useWrappedStore(rest);
const {pageProps} = props;
return (
<Provider store={store}>
<ApolloProvider client={client}>
<GlobalStyle/>
<ThemeProvider theme={theme}>
<LookyHead />
<Component {...pageProps} />
</ThemeProvider>
</ApolloProvider>
</Provider>
);
}
export default App;
postSlice.ts
//
some imports and interfaces...
//
export const postSlice = createSlice({
name: 'post',
initialState,
reducers: {
setPost: (state, action) => {
state.post = action.payload
}
},
extraReducers: {
[HYDRATE]: (state, action) => {
return {
...state,
...action.payload.post,
};
},
},
});
export const { setPost } = postSlice.actions;
export default postSlice.reducer;
Post component of posts grid on profile, here i have link to post/[id]
function Post({previewUrl, likesCount, commentsCount, duration, id}: Props) {
some code...
return (
<Link href={`/post/${id}`}>
<Container
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
>
<img src={previewUrl} alt="image"/>
<PostFooter
isHover={isHover}
likesCount={likesCount}
commentsCount={commentsCount}
time={time}
/>
</Container>
</Link>
);
}
export default memo(Post);
getServerSideProps in post/[id]
export const getServerSideProps =
wrapper.getServerSideProps(
(store) =>
async ({query}) => {
const id = query!.id as string
try {
const {data} = await client.query<Res, Vars>({
query: GET_POST,
variables: {
postId: id
}
});
console.log(data.publicPost) // Here I have data!
store.dispatch(setPost(data.publicPost))
} catch (e) {
console.log(e)
}
return {
props: {}
}
})
export default Post;
Data component inside post/[id], where client crash
//
imports...
//
function Data() {
const {post} = useAppSelector(({post}) => post) // looks weird but its ok
const parsed = parseISO(post?.createdAt) // Here my client fall
const date = format(parsed, 'dd MMMM yyyy, HH:MM', {locale: enGB})
return (
...
);
}
export default Data;
When the state in Redux is updated, if it is different from the useState of the current page, the useState in the page will be updated, but the value in the red box will not change after the update, it is always the default true.
import { useEffect, useState } from "react";
import store from "./redux";
import { useDispatch } from "react-redux";
import { setState } from "./redux/modules/menu";
function App() {
const dispatch = useDispatch();
const [baseState, setBaseState] = useState(true);
useEffect(() => {
console.log("baseState :>> ", baseState);
}, [baseState]);
useEffect(() => {
const unSubscribe = store.subscribe(() => {
console.log(store.getState().menu.state, baseState);
if (store.getState().menu.state !== baseState) {
setBaseState(store.getState().menu.state);
}
});
return () => unSubscribe();
}, []);
const buttonEvent = () => {
const storeState = store.getState().menu.state;
dispatch(setState(!storeState));
};
return (
<div className="App">
<h1>value: {baseState + ""}</h1>
<button onClick={buttonEvent}>change</button>
</div>
);
}
export default App;
run result :
So if i got your question, what you are doing is, on a button click, you are changing the redux state and on that basis, you want to change the local state.
But in your useEffect() (2nd one);
useEffect(() => {
const unSubscribe = store.subscribe(() => {
console.log(store.getState().menu.state, baseState);
if (store.getState().menu.state !== baseState) {
setBaseState(store.getState().menu.state);
}
});
return () => unSubscribe();
}, []);
You are giving an empty array as 2nd argument, which means this useEffect is going to get triggered only on the first render(similar to componentDidMount in class components), thus will never know if the redux state has changed.
Thus, to make it working, just remove them (like we have componentDidUpdate in class components).
I found the most appropriate solution, using Redux useSelector
Im using next-redux-wrapper and I get following 2 type errors. Anyone encounter this before and how do you solve it? All else, the code is working and i can see my state in my redux-devtools-extension. Thanks
import { useSelector } from "react-redux";
import Layout from "../components/Layout";
import { getProjects } from "../redux/actions/projectActions";
import { wrapper } from "../redux/store";
const Redux = () => {
// Warning 1: Property 'allProjects' does not exist on type 'DefaultRootState'.ts(2339)
const { projects } = useSelector((state) => state.allProjects);
console.log("res", projects);
return (
<Layout>
<h1>Redux</h1>
</Layout>
);
};
export const getServerSideProps =
// Warning 2: Type '({ req }: GetServerSidePropsContext<ParsedUrlQuery>) => Promise<void>' is not assignable to type 'GetServerSideProps<any, ParsedUrlQuery>'.
Type 'Promise<void>' is not assignable to type 'Promise<GetServerSidePropsResult<any>>'.
Type 'void' is not assignable to type 'GetServerSidePropsResult<any>'.ts(2322)
wrapper.getServerSideProps(
(store) =>
async ({ req }) => {
(await store.dispatch(getProjects(req))) as any;
}
);
export default Redux;
I did below and the ts warnings went away
I m not sure if it's correct though but so far seems ok
import { RootStateOrAny, useSelector } from "react-redux";
import Layout from "../components/Layout";
import { getProjects } from "../redux/actions/projectActions";
import { wrapper } from "../redux/store";
const Redux = () => {
const { projects } = useSelector(
(state: RootStateOrAny) => state.allProjects
);
console.log("res", projects);
return (
<Layout>
<h1>Redux</h1>
</Layout>
);
};
export const getServerSideProps = wrapper.getServerSideProps(
(store) =>
async ({ req }) => {
await store.dispatch(getProjects(req));
return {
props: {},
};
}
);
export default Redux;
I am using react-redux with redux and redux-toolkit. And according to this example, i created an async dispatch that calls the reducer action when resolved.
import { createSlice } from "#reduxjs/toolkit";
import axios from "axios";
export const BlogSlice = createSlice({
name: "Blog",
initialState: {
BlogList: null,
},
reducers: {
getBlogList: (state, action) => {
console.log(action.payload);
state.BlogList = action.payload;
}
},
});
export const { getBlogList } = BlogSlice.actions;
export const getBlogListAsync = (user_id) => (dispatch) => {
axios.get(`/api/blog/getblogs/${user_id}`).then((res) => {
console.log(res.data);
dispatch(getBlogList(res.data.result));
});
};
export const selectBlogList = (state) => state.Blog.BlogList;
export default BlogSlice.reducer;
I have used it in a component accordingly so that, the component dispatches getBlogListAsync and that logs the res.data but getBlogList is not being dispatched. I tried putting other console.log() but don't understand what is wrong.
A similar Slice is working perfectly with another Component.
It is hard to say for sure what's wrong here because there is nothing that is definitely wrong.
res.data.result?
You are logging res.data and then setting the blog list to res.data.result. My best guess as to your mistake is that res.data.result is not the the correct property for accessing the blogs, but I can't possibly know that without seeing your API.
console.log(res.data);
dispatch(getBlogList(res.data.result));
missing middleware?
Is there any chance that "thunk" middleware is not installed? If you are using Redux Toolkit and omitting the middleware entirely, then the thunk middleware will be installed by default. Also if this were the case you should be getting obvious errors, not just nothing happening.
it seems fine...
I tested out your code with a placeholder API and I was able to get it working properly. Maybe this code helps you identify the problem on your end. Code Sandbox Demo.
import React from "react";
import { createSlice, configureStore } from "#reduxjs/toolkit";
import axios from "axios";
import { Provider, useDispatch, useSelector } from "react-redux";
export const BlogSlice = createSlice({
name: "Blog",
initialState: {
BlogList: null
},
reducers: {
getBlogList: (state, action) => {
console.log(action.payload);
state.BlogList = action.payload;
}
}
});
export const { getBlogList } = BlogSlice.actions;
const store = configureStore({
reducer: {
Blog: BlogSlice.reducer
}
});
export const getBlogListAsync = (user_id) => (
dispatch: Dispatch
) => {
// your url `/api/blog/getblogs/${user_id}`
const url = `https://jsonplaceholder.typicode.com/posts?userId=${user_id}`; // placeholder URL
axios.get(url).then((res) => {
console.log(res.data);
// your list: res.data.result <-- double check this
const list = res.data; // placeholder list
dispatch(getBlogList(list));
});
};
export const selectBlogList = (state) => state.Blog.BlogList;
const Test = () => {
const dispatch = useDispatch();
const blogs = useSelector(selectBlogList);
const user_id = "1";
return (
<div>
<button onClick={() => dispatch(getBlogListAsync(user_id))}>
Load Blogs
</button>
<h3>Blog Data</h3>
<div>{JSON.stringify(blogs)}</div>
</div>
);
};
export default function App() {
return (
<Provider store={store}>
<Test />
</Provider>
);
}