How to NOT persist state between Next.js dynamic routes? - javascript

I'm building a headless eCommerce site using React/Next and have a [product].js dynamic route which is used to generate all product pages, using getStaticPaths() and getStaticProps() which generates the pages fine.
I'm using useState hook within [product].js to manage a number input (for quantity) and a couple of other things.
The first product page loaded works fine, but when I go to other product pages, they use the same state from the first product.
Is there a way to have the state NOT persist between route changes?
Through some digging, I found that this is an issue with next and is in their backlog. It essentially stems from the fact that the component doesn't have a key. This means switching between routes on the same dynamic route doesn't register correctly and causes the component to use stale state.
A possible solution I found was this:
export async function getStaticProps({params}) {
const props = await getData(params);
// key is needed here
props.key = data.id;
return {
props: props
}
}
This is my implementation which doesn't work for me:
export default function ProductPage(props) {
// this state doesn't reset between dynaic route changes
const [quantity, setQuantity] = useState(1)
return(
...
)
}
export async function getStaticProps({ params }) {
const slug = params.product
const props = await client.query({
query: singleProductQuery,
variables: { id: slug }
})
props.key = props.data.product.slug
return {
props: props
}
}
I tried wrapping the contents within another component and adding a key to that, like so:
return(
<OuterComponent key={props.id}>
// components within here, that have their own state, now work
</OuterComponent>
)
Since this new keyed component is only in the return statement and does not encapsulate the state hook, it does not work. This does reset the state however, for any components found within wrapped component.

You can use useEffect hook and useRouter hook at dynamic router to reset the state.
import {useState, useEffect} from 'react'
import {useRouter} from 'next/router'
const ProductPage = (props) => {
const [state, setState] = useState(someState)
const dynamicRoute = useRouter().asPath
useEffect(() => {
setState(resetState) // When the dynamic route change reset the state
}, [dynamicRoute])
//Some other logic
return (
......
)
}

It seems that you've encountered the same issue thread that I've found:
https://github.com/vercel/next.js/issues/9992
It seems from what I've read that to fix your case, all you need to do is change your getStaticProps to return an object with a unique key:
export async function getStaticProps({ params }) {
const slug = params.product
const props = await client.query({
query: singleProductQuery,
variables: { id: slug }
});
return {
props: props,
key: slug
}
}
What you've been doing previously is passing a key to the props object instead of root return object for getStaticProps

You can use useEffect hook to reset state
export default function ProductPage(props) {
// this state doesn't reset between dynaic route changes
const [quantity, setQuantity] = useState(1)
useEffect(() => {
setQuantity(props.quantity) // <-- this props comes from getStaticProps
}, [props]) // <--- useEffect will keep tracking changing props
return(
...
)
}
So when your props changes - your state updates.

Related

How can I assign data from Http response in const Component?

My goal is to ultimately create add a shopping cart functionality using context API. In order to do so I need to get the products from my database and store it in an array.
Currently, the challenge I'm facing is how to retrieve the data from the axios response and store it in a variable that will be passed on within a const component. Apparently, the issue is that the variable gets passed to the child before the Axios response is complete.
I tried using the await keyword, but got an error regarding not being in an async function. Hence, I tried plugging the async keyword but that didn't work as it yielded errors.
I was able to retrieve data from axios within class components with success, however, I am unable to do so in these const.
Here is my code:
Context.js
import { createContext, useContext, useReducer } from "react";
import React from "react";
import ProductService from "../services/ProductService";
import { cartReducer } from "./Reducers";
const Cart = createContext();
const Context = ({ children }) => {
let products = [];
console.log("part1", products);
ProductService.getAllProducts().then((res) => {
products = res.data; //Also tried setState({ products : res.data})
console.log("response: ", products);
});
const [state, dispatch] = useReducer(cartReducer, {
products: products,
cart: [],
});
return <Cart.Provider value={{ state, dispatch }}>{children}</Cart.Provider>;
};
export const CartState = () => {
return useContext(Cart);
};
export default Context;
ProductService.js
import axios from "axios";
const PRODUCT_BASE_URL = "http://localhost:8080/api/v1/";
class ProductService {
getAllProducts() {
return axios.get(PRODUCT_BASE_URL + "products");
}
getProductsByCategory(category) {
return axios.get(PRODUCT_BASE_URL + "products/" + category);
}
getProductById(id) {
return axios.get(PRODUCT_BASE_URL + "product/" + id);
}
}
export default new ProductService();
Reducers.js
export const cartReducer = (state, action) => {
switch (action.type) {
default:
return state;
}
};
HomeComponenet.jsx
import React from "react";
import SlideShowComponent from "./SlideShowComponent";
import HomeCategoriesComponent from "./HomeCategoriesComponent";
import FeaturedProductsComponent from "./FeaturedProductsComponent";
import { CartState } from "../context/Context";
// class HomeComponent extends React.Component
function HomeComponent() {
const { state } = CartState();
console.log("Cart Inside the Home Component: ", state);
return (
<>
<SlideShowComponent />
<div>
<HomeCategoriesComponent />
</div>
<div>
<FeaturedProductsComponent />
</div>
</>
);
}
export default HomeComponent;
First of all, I've created a fixed example here: https://stackblitz.com/edit/react-jqtcia?file=src/Context.js
You are correct, you need to await the result from axios. In React, we use the useEffect hook for things with side effects or that should not be done as part of the render. Renders in react should be non blocking, that is they should not be dependent on things like data fetching.
A simple example of this would be if we needed it in local state. This example renders without the data, then re-renders once the data is available.
const [products, setProducts] = useState([]);
useEffect(async () => {
const { data } = await ProductService.getAllProducts();
setProducts(data);
}, []);
return <div>{products.length > 0 ? `${products.length} products` : 'Loading...'</div>
NOTE: the , []); means that this will fire once, when the first render happens.
This fixes the first part of your problem, getting the result out of the request/axios.
However, the second part and most important part is that you weren't using this value. You were attempting to insert the result as part of the initial state, but this was empty by the time it was created. As you are using reducers (useReducer), this means you need to dispatch an action for each event and handle all the relevant events to data fetching in the reducer. That means, you should need to be able to handle:
Some data is in a loading state (e.g., pagination or first load)
The data failed to load
The data is partially loaded
The data has fully loaded
I've created a minimal happy example (there is no pagination and data fetching always succeeds):
Context.js
useEffect(async () => {
const { data: loadedProducts } = await ProductService.getAllProducts();
console.log('response: ', JSON.stringify(loadedProducts));
dispatch({ type: 'PRODUCTS_LOADED', products: loadedProducts });
console.log(state);
}, []);
Reducers.js
export const cartReducer = (state, action) => {
console.log(action);
switch (action.type) {
case 'PRODUCTS_LOADED':
const newState = { ...state, products: action.products };
console.log(newState);
return newState;
default:
return state;
}
};
In fact, if you'd done this with your original code, it would've worked:
ProductService.getAllProducts().then((res) => {
dispatch({ type: 'PRODUCTS_LOADED', products : res.data});
console.log("response: ", products);
});
However, this has a different bug: It will refetch the data and then dispatch the event each time Context.js is re-rendered.
Since you've asked for more information in your comment, I'll provide it here (comments were not big enough).
I've linked the relevant API documentation for the hooks above, these provide pretty good information. React does a pretty good job of explaining the what, and why of these hooks (and the library itself). Seriously, if you haven't read their documentation, you should do so.
Additional resources:
What, when and how to use useEffect - In short, if you have something to do that isn't rendering such as data fetching, it's a side-effect and should be in a useEffect.
What is a reducer in JavaScript/React/Redux - Reducers are a pattern to make shared state easier to manage and test. The basic idea is that you define a reducer, which takes an initial state and an event/action, and produces a new state. The key idea is that there must be no side-effects, the same action and state will always produce the same result, no matter the date/time, network state, etc, etc. This makes testing and reasoning about things easier, but at the cost of a more complex state management.
However, something important that I ignored in your original question is that you are kind of reinventing the wheel here. There is already a library that will centralise your state and make it available via context, it's the library that originally invented reducers: redux. I'm guessing you have read something like this article about using context instead of redux, however the advantage of redux for you is that there is a litany of documentation about how to use it and solve these problems.
My recommendation for you is to make sure you need/want redux/reducers. It has it's value and I personally love the pattern but if you are just getting started on React, you would be better off just using useState in my opinion.

Redux: mapStateToProps is rendering twice

I have started learning Redux recently, and something is bugging me.
import React, { useEffect } from "react";
import { connect, useDispatch } from "react-redux";
import Modal from "../Modal/Modal";
import history from "../../utils/history";
import { fetchPost } from "../../actions";
const PostDelete = ({ match, post }) => {
const postId = match.params?.id;
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchPost(postId));
}, [dispatch]);
return (
<Modal
/>
);
};
const mapStateToProps = (state, { match }) => {
console.log("MSTP", state.posts[match.params?.id]) // <== CONSOLED TWICE !!
return { post: state.posts[match.params?.id] };
};
export default connect(mapStateToProps, {})(PostDelete);
When I navigate to this using react-router, as per my understanding:
MSTP should be called first(which fetches the post from the store)
Then useEffect() fetches the post(just in case user directly opens this page)
It dispatches the action which changes the state
This re-renders the MSTP again
Is there a way to get around this? Is this a bad approach or am I missing something here?
Explanation
First of all I'd like to say your understanding of what's happening is correct. From the official react-redux documentation it describes that mapStateToProps is called every time the store is updated.
This is ok if you have a fairly simple mapStateToProps object to compute, but can cause performance degradations if you're doing something more intensive. For intensive cases I'd recommend using a memoized selector, which will just return the previously calculated mapStateToProps value, without doing any new computations, if no relevant changes were made to the store. A good library for achieving this is reselect.
Even with a memoized selector, your console.log('MSTP') statement will be printed, but the underlying computation will be quicker.
Code Example
Consider the following example.
Component is rendered for the first time
useEffect fetches the post and updates the store at state.posts (relevant to this component)
Some other component updates the redux state, at an irrelevant part to this component, e.g. state.comments
Here's the code and console output BEFORE using a memoized selector
const intensivePostsFormatting = (state) => {
console.log('Formatting Posts');
// do some stuff with state.posts
return formattedPosts;
}
const mapStateToProps = ({ match, state }) => {
console.log('MSTP');
return {
posts: intensivePostsFormatting(state)
}
}
// Console Output:
// MSTP
// Formatting Posts
// MSTP
// Formatting Posts
// MSTP
// Formatting Posts
Here's the code and output AFTER using a memoized selector
import { createSelector } from 'reselect';
const intensivePostsFormatting = (posts) => {
console.log('Formatting Posts');
// do some stuff with posts
return formattedPosts;
}
const postsSelector = createSelector(
state => state.posts,
posts => intensivePostsFormatting(posts)
)
const mapStateToProps = ({match, state }) => {
console.log('MSTP');
return {
posts: postsSelector(state)
}
}
// Console Output:
// MSTP
// Formatting Posts
// MSTP
// Formatting Posts
// MSTP
Note that the difference between the before and after, is that "Formatting Posts" is logged 3 times in the "before" example and 2 times in the "after" example. This is because using a memoized selector allowed us to skip computing the formatted posts when a change to something other than state.posts was made.

How can I use a provider value to useMutation and also dispatch the changes to state afterwards?

I would like to use a Context.Provider value to handle both mutating and dispatching similar changes. I have read about React-Apollo's onComplete method, but I'm not sure which approach will cover more cases where I need to both mutate and dispatch state. Here's what I have:
const CartContext = React.createContext<{
state: State
dispatch: Dispatch<AnyAction>
cartApi: any
}>({ state: initialState, dispatch: () => null, cartApi: mutateUserProductsAndUpdateCart })
function CartProvider({ children }: { children?: React.ReactNode }) {
const [state, dispatch] = useReducer<Reducer<State, AnyAction>>(reducer, initialState)
// feel like i need to do something with the hook here to avoid invariant violation side effects
const [updateUserProducts] = useUpdateUserProducts()
return (
<CartContext.Provider value={{ state, dispatch, cartApi: mutateUserProductsAndUpdateCart}}>
{children}
</CartContext.Provider>
)
}
export const useCartState = () => useContext(CartContext)
And here's what I would like to do with my mutateUserProductsAndUpdateCart:
const mutateUserProductsAndUpdateCart = async (_mutation: any, _mutationParams: any, _dispatchObject: AnyObject) => {
// apollo mutation
const updateUserProductsResult = await updateUserProducts(_mutationParams)
if (updateUserProductsResult.error) throw Error("wtf")
// useReducer dispatch
dispatch(_dispatchObject)
return
}
and here is how I would like to access this on another component:
const { cartApi } = useCartState()
const addProductToCart = async () => {
const result = await cartApi({
mutation,
mutationVariables,
dispatchObject})
}
I feel like this article is sort of the direction I should be taking, but I'm very lost on implementation here. Thanks for reading.
I'm not sure this directly answers your question, but have you considered just using Apollo Client? It looks like you are trying to do two things:
Save items added to the cart to the server
Update the cart locally in the cache
It seems like you could skip creating your own context altogether and just create a hook for mutating (saving your cart items) and then update your local cache for cart items. Something like this:
import gql from 'graphql-tag';
import useMutation from '#apollo/client';
export const mutation = gql`
mutation($items: [CartItem]!) {
saveCartItems(items: $items) {
id
_list_of_properties_for_cache_update_
}
}
`;
export const useSaveCartItems = mutationProps => {
const [saveCartItems, result] = useMutation(
mutation,
mutationProps
);
return [
items => {
saveCartItems({
update: (cache, { data }) => {
const query = getCartQuery; // Some query to get the cart items from the cache
const results = cache.readQuery({ query });
// We need to add new items only; existing items will auto-merge
// Get list of new items from results
const data = []; // List of new items; IMPLEMENT ME!!!
cache.writeQuery({ query, data });
},
variables: { items },
});
},
result,
];
};
Then in your useCartState hook you can just query the local cache for the items using the same query you used for the update and return that. By using the update function you can fix your local cache and anybody can access it from anywhere, just use the hook. I know that isn't exactly what you asked for, but I hope it helps.
Apollo client documentation on handling this may be found here.

Destructured object as function parameter

I don’t understand the parameter of const Posts below. I’m fairly new to node/React. Is it a destructured parameter object? Or is it just an object being passed as a parameter?
getPosts and post are showing as undefined. But I don’t understand where the parameter object is being passed from into the function...
Full code here: https://github.com/bradtraversy/devconnector_2.0/blob/master/client/src/components/posts/Posts.js
Thanks in advance!!
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import PostItem from './PostItem';
import PostForm from './PostForm';
import { getPosts } from '../../redux/actions/post';
const Posts = ({ getPosts, post: { posts, loading } }) => {
useEffect(() => {
getPosts();
}, [getPosts]); ```
So Posts is a React Function component.
All Function components will receive a props object as its first argument.
const Posts = (props) => { /* ... */ }
props will always be an object containing the props that were passed into it when the component was rendered, for example:
import Posts from './path/to/Posts'
function SomeParentComponent() {
return <Posts limit={10} categories={{news:true, sports:false}} />
}
In this case props will be an object that looks like this:
{
limit : 10,
categories : {
news : true,
sports : false,
}
}
You can of course destructure the props object in your component:
const Posts = (props) => {
const {
limit,
categories
} = props
// ... other stuff
}
But you can go even further and do what's called "unpacking" in order to destructure nested properties
const Posts = (props) => {
const {
limit,
categories : {
sports,
news
}
} = props
// ... other stuff
}
Lastly, instead of doing that in the function body, you can destructure and unpack objects in-line where the arguments are for the same result.
const Posts = ({limit, categories:{news,sports}}) => {
// ... other stuff
}
Which is what your code sample is doing.
It appears it's expecting the parent component to pass in a function as the getPosts prop, which when called will first set posts.loading to true, load the posts, then set posts.loading to false. Ex:
function SomeParentComponent() {
const [loading, setLoading] = useState(false)
const [posts, setPosts] = useState([])
const loadPosts = useCallback(async () => {
setLoading(true)
const loadedPosts = await loadPostsSomehow()
setPosts([posts, ...loadedPosts])
setLoading(false)
}, [])
return <Posts getPosts={loadPosts} post={{posts, loading}} />
}
Make sure to use useCallback to get a memoized callback here or you will get stuck in an infinite loop
**EDIT**
After actually looking at the link provided, it's slightly different actually. Instead of the post object being provided by the parent component, it's actually provided by redux, but the logic is essentially the same. The difference is that instead of the parent component changing the loading and post state, it's done via redux state management.
Yes, it is de-structured function parameter object.
In your case parameters to Posts pass through Redux connect() function.
const mapStateToProps = state => ({
post: state.post
});
export default connect(
mapStateToProps,
{ getPosts }
)(Posts);
Check your getPosts import and make sure it is not undefined.
import { getPosts } from '../../actions/post';
Also check your redux state and make sure it has state.post.

React Hooks and ActionCable

Trying to get along with React new Hooks and ActionCable, but stuck with the problem that I can't get the right data in Rails when trying to send state.
I've tried to use send() method immediately after doing setState() and send my updated data, but for some reason, the data which received on the Rails part is old.
For example, if I put "Example" to the input I'll see "{"data"=>"Exampl"} on the Rails side. I suppose the data update the state later than my request goes.
If I send() value from e.target.value everything works fine
Therefore I've tried to use new useEffect() hook and send data there. But I get only data when rendering the page. Afterward, I don't get anything and sometimes get error RuntimeError - Unable to find subscription with an identifier. Seems like effect hook sends data too early or something.
I'm pretty new to Hooks and WebSockets. Would love to get any help here. I can share Rails code, but there is only a receiver and nothing else.
First exmaple:
import React, { useState, useEffect } from "react"
import ActionCable from 'actioncable'
function Component(props) {
const [data, setData] = useState("");
const cable = ActionCable.createConsumer('ws://localhost:3000/cable');
const sub = cable.subscriptions.create('DataChannel');
const handleChange = (e) => {
setData(e.target.value)
sub.send({ data });
}
return (
<input value={data} onChange={handleChange}/>
)
}
Tried to useEffect and move send() there:
useEffect(() => {
sub.send({ data });
}, [data]);
I'd love to find a way to correctly use React and ActionCable. And use hooks if it's possible.
I was trying an approach similar to Oleg's but I could not setChannel inside the action cable create subscription callback. I had to setChannel outside of the callback but within the useEffect hook. Below is the solution that worked for me.
create consumer in index.js and provide the consumer through Context to App.
index.js
import React, { createContext } from 'react'
import actionCable from 'actioncable'
... omitted other imports
const CableApp = {}
CableApp.cable = actionCable.createConsumer('ws://localhost:3000/cable')
export const ActionCableContext = createContext()
ReactDOM.render(
<Router>
... omitted other providers
<ActionCableContext.Provider value={CableApp.cable}>
<App />
</ActionCableContext.Provider>
</Router>,
document.getElementById('root')
)
Use the cable context in your child component and create subscription in useEffect hooks; unsubscribe in clean up
import React, { useState, useEffect, useContext } from 'react'
import { useParams } from 'react-router-dom'
... omitted code
const [channel, setChannel] = useState(null)
const { id } = useParams()
const cable = useContext(ActionCableContext)
useEffect(() => {
const channel = cable.subscriptions.create(
{
channel: 'MessagesChannel',
id: id,
},
{
received: (data) => {
receiveMessage(data)
},
}
)
setChannel(channel)
return () => {
channel.unsubscribe()
}
}, [id])
const sendMessage = (content) => {
channel.send(content)
}
You can register your cable at root component like that:
import actionCable from 'actioncable';
(function() {
window.CableApp || (window.CableApp = {});
CableApp.cable = actionCable.createConsumer('ws://localhost:3000/cable')
}).call(this);`
so it will be available as global variable;
and then in any component where you want to create channel and send data:
const [channel, setChannel] = useState(null);
useEffect(() => {
CableApp.cable.subscriptions.create(
{
channel: 'YourChannelName',
},
{
initialized() {
setChannel(this)
},
},
);
}, []);
return <button onClick={() => channel.send(some_data)} >Send counter</button>
Your problem is here:
const handleChange = (e) => {
setData(e.target.value)
sub.send({ data });
}
setData is like setState in that the state is only updated after the render i.e. after the function has exited. You are sending the current data not the new data. Try this:
const handleChange = (e) => {
const newData = e.target.value;
setData(newData)
sub.send({ data: newData });
}

Categories