Why is my server storage not syncing with the client one in Next.js app? - next.js

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;

Related

wrapper.getInitialPageProps from next-redux-wrapper don't return the props to the component

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;

Refactoring reducers and actions in a feature based architecture style

This is a purely hypothetical example while I am trying to learn NextJs and I added Redux following the documentation from the official link
but I want to format the code in a certain way, similar to feature based architecture where each feature is encapsulated in a separate folder.
Please feel free to add, remove and suggest any changes, they are all most welcoming.
This is my general folder structure
components
public
pages
|-accounts
| |-index.tsx
|-_app.tsx
|-index.tsx
store
|-account
| |-accountsApi.ts
| |-actions.ts
| |-index.ts
| |-reducer.ts
| |-types.ts
|-index.ts
styles
package.json
...
And this is the Accounts component under accounts folder at pages.
I am dispatching the login action from action creators, is there any way to make the action accept an object with the credentials?
//pages/account/index.tsx
import Link from 'next/link'
import { useState } from 'react'
import Layout from '../../components/Layout'
import { useDispatch } from "react-redux"
import type { Account } from '../../store/account/types'
import { accoutActions } from '../../store/account'
type Props = {
accounts: Account[]
}
const Accounts: React.FunctionComponent<Props> = ({ accounts }) => {
const dispatch = useDispatch();
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = (event: any) => {
let obj = {
username: username,
password: password
}
dispatch(accoutActions.login(obj)); // how to set it up, so that the action accepts an object with credentials
}
return (
<Layout title="Accounts | Demo">
<h1>Accounts</h1>
<p>You are currently on: /accounts</p>
<p>
<Link href="/">
<a>Go home</a>
</Link>
</p>
<div>
<p>Login</p>
<div>
<label htmlFor="username">Username:</label>
<input onChange={e => setUsername(e.target.value)} type="text" id="username" name="username" value={username} />
<label htmlFor="password">Last name:</label>
<input onChange={e => setPassword(e.target.value)} type="text" id="password" name="password" value={password} />
<button type="submit" onClick={handleSubmit}>Login</button>
</div>
</div>
</Layout>
)
}
export default Accounts
Below is my account store with its respective files:
accountsAPI.ts where the api calls to the server are made, I will try to refactor later in a singleton pattern using axios
however I am learning what NextJs allows and what not.
import { CreateAccountInputModel, LoginInputModel } from "./types"
const url = 'https://127.0.0.1:8888/api/accounts'
export const createAccount = async (objModel: CreateAccountInputModel): Promise<any> => {
const response = await fetch(`${url}/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(objModel),
})
const result = await response.json()
return result
}
export const loginAccount = async(objModel: LoginInputModel): Promise<any> => {
const response = await fetch(`${url}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(objModel),
})
const result = await response.json()
return result
}
export const logoutAccount = async () : Promise<any> => {
const response = await fetch(`${url}/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
const result = await response.json()
return result
}
actions.ts contains account actions
import { createAction } from '#reduxjs/toolkit'
import { AccountActionTypes } from './types'
const create = createAction(AccountActionTypes.CREATE)
const login = createAction(AccountActionTypes.LOGIN)
const logout = createAction(AccountActionTypes.LOGOUT)
export const actionCreators = {
create,
login,
logout,
};
index.ts where I combine reducer from the reducer file and make a slice so that I can join all reducers to the store
I want to be able the reducers in a separate file
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { CreateAccountInputModel } from './types'
import type { AppState, AppThunk } from '..'
import { createAccount, loginAccount, logoutAccount } from './accountsAPI'
import { reducer, initialState } from './reducer'
export const createAsync = createAsyncThunk(
'account/create',
async (objModel: CreateAccountInputModel) => {
const response = await createAccount(objModel)
// The value we return becomes the `fulfilled` action payload
return response.data
}
)
export const accountSlice = createSlice({
name: 'account',
initialState,
reducers: reducer, // error: Type 'ReducerWithInitialState<AccountState>' is not assignable to type 'ValidateSliceCaseReducers<AccountState, SliceCaseReducers<AccountState>>'.
extraReducers: (builder) => {
builder
.addCase(createAsync.pending, (state) => {
state.status = 'loading'
})
.addCase(createAsync.fulfilled, (state, action) => {
state.status = 'idle'
})
},
})
export { actionCreators as accoutActions } from './actions';
export const isAuthenticated = (state: AppState) => state.account.isAuthenticated
export default accountSlice.reducer
Here are reducers reducer.ts and the initial state of the given reducer
import { createReducer, PayloadAction } from '#reduxjs/toolkit'
import { actionCreators } from './actions';
import { Account, AccountState } from './types';
export const initialState: AccountState = {
isAuthenticated: false,
token: '',
status: 'idle',
accounts: [] as Account[],
}
export const reducer = createReducer(initialState, (builder) => {
builder
.addCase(actionCreators.login, (state, action: PayloadAction<any>) => {
state.isAuthenticated = action.payload.isAuthenticated
state.token = action.payload.token
// can I do, return { ...state, ...} so that the previous state is preserved?
})
.addCase(actionCreators.logout, (state, action) => {
state.isAuthenticated = false
})
.addCase(actionCreators.create, (state, action: PayloadAction<any>) => {
state.accounts = [...state.accounts, action.payload]
}).addDefaultCase((state, action) => {
state.isAuthenticated = false
state.token = ''
})
})
All the types and models are stored in types.ts file.
I can write types.ts if it is required, but you can assume a general case of an account.
is there any way to make the action accept an object with the credentials?
const login = createAction<TypeOfObjectWithCredentials>(AccountActionTypes.LOGIN)
Calling login with an object will put it inside the payload automatically.

useEffect hook problem, use redux to get data from backend to frontend

Hello i am new to programming, and i have learn to use MERN to make a sign up,i have no issue with backend but when i tried to use redux ,i have this problem in the frontend
enter image description here
This is the code for the SignInScreen.js
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { login } from '../actions/userAction'
export const SignInScreen = (props, history) => {
const navigate = useNavigate
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const submitHandler = (e) => {
e.preventDefault()
dispatch(login(email, password))
}
const { search } = useLocation()
const redirectInUrl = new URLSearchParams(search).get('redirect')
const redirect = redirectInUrl ? redirectInUrl : '/'
const userlogin = useSelector((state) => state.userlogin)
const { userInfo, loading, error } = userlogin
const dispatch = useDispatch()
useEffect(() => {
if (userInfo) {
navigate(redirect)
}
}, [navigate, userInfo, redirect])
I dont know what wrong with the code,but i do know that it connected with the redux store which have reducer,action and constant..this is for the redux store.js
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import {
userLoginReducer,
} from './reducers/userReducer'
const userInfo = localStorage.getItem('userInfo')
? JSON.parse(localStorage.getItem('userInfo'))
: null
const initialState = {
userLogin: { userInfo },
}
const reducer = combineReducers({
userLogin: userLoginReducer,
})
const middleware = [thunk]
const store = createStore(
reducer,
initialState,
compose(applyMiddleware(...middleware))
)
export default store
This is for constant
export const USER_LOGIN_REQUEST = 'USER_LOGIN_REQUEST'
export const USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS'
export const USER_LOGIN_FAIL = 'USER_LOGIN_FAIL'
export const USER_LOGOUT = 'USER_LOGOUT'
this is userReducer.js
import {
USER_LOGIN_REQUEST,
USER_LOGIN_SUCCESS,
USER_LOGIN_FAIL,
USER_LOGOUT,
} from '../constants/userConstant'
function userLoginReducer(state = {}, action) {
switch (action.type) {
case USER_LOGIN_REQUEST:
return { loading: true }
case USER_LOGIN_SUCCESS:
return { loading: false, userInfo: action.payload }
case USER_LOGIN_FAIL:
return { loading: false, error: action.payload }
case USER_LOGOUT:
return {}
default:
return state
}
}
export userLoginReducer
and lastly for user.js
import Axios from 'axios'
import {
USER_LOGIN_REQUEST,
USER_LOGIN_SUCCESS,
USER_LOGIN_FAIL,
} from '../constants/userConstant'
const login = (email, password) => async (dispatch) => {
try {
dispatch({ type: USER_LOGIN_REQUEST })
const config = { headers: { 'Content-Type': 'application/json' } }
const { data } = await Axios.post(
'/api/users/login',
{ email, password },
config
)
dispatch({ type: USER_LOGIN_SUCCESS, payload: data })
localStorage.setItem('userInfo', JSON.stringify(data))
} catch (error) {
dispatch({
type: USER_LOGIN_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
})
}
}
export login
I just want to get the data into the userInfo but it dont recognized them and said it is a TypeError..I hope u can help me with this..im using the redux latest version
The problem is in userLoginReducer. Each reducer should return a complete new copy of the store.
If you return just the changes, the object you return replaces the entire state.
For example in this code:
switch (action.type) {
case USER_LOGIN_REQUEST:
return { loading: true };
}
The state { userLogin: { userInfo } } will be replaced with { loading: true }. Then you will not have userLogin anymore in the state. That's why you get the error.
To overcome this problem, spread the previous state in returned object (for all actions):
switch (action.type) {
case USER_LOGIN_REQUEST:
return { ....state, loading: true }; // ...state copies exist state to the new copy of state
}
Note: To easily solve similar bugs in the future, I recommend to use redux devtools extension. It is a great extension for debugging and look at changes in redux store.
I have tried that but still cannot fix my problem,this is the rest of my signinScreen,js
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { login } from '../actions/userAction'
export const SignInScreen = (props, history) => {
const navigate = useNavigate
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const submitHandler = (e) => {
e.preventDefault()
dispatch(login(email, password))
}
const { search } = useLocation()
const redirectInUrl = new URLSearchParams(search).get('redirect')
const redirect = redirectInUrl ? redirectInUrl : '/'
const userlogin = useSelector((state) => state.userlogin)
const { userInfo, loading, error } = userlogin
const dispatch = useDispatch()
useEffect(() => {
if (userInfo) {
navigate(redirect)
}
}, [navigate, userInfo, redirect])
return (
<Container className="small-container">
<h1 className="my-3">Sign In</h1>
<Form onSubmit={submitHandler}>
<Form.Group className="mb-3" controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
required
onChange={(e) => setEmail(e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-3" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
required
onChange={(e) => setPassword(e.target.value)}
/>
</Form.Group>
<div className="mb-3">
<Button type="submit">Sign In</Button>
</div>
<div className="mb-3">
New Customer?{' '}
<Link to={`/signup?redirect=${redirect}`}>Create new account</Link>
</div>
</Form>
</Container>
)
}
it still show the same error like before,i have no idea to solve this

Why Redux action is not Being being dispatched in Redux-Tooklit

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>
);
}

Redux Wait for Async Action to Fnish

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} />
);
}

Resources