dom is not updated after state change via Context API - reactjs

I am quite new to React and tried to finish a project on FrontendMentor. It's basically a simple OPA of an online store with just one product and the ability to add this product to a cart.
I thought it would be a good idea to learn about Reacts Context API so that I get a basic understanding.
The first context that I created basically looks like this:
import { createContext, useContext, useState } from "react";
export const CartContext = createContext();
export const CartProvider = ({ children }) => {
const cartValue = useCartProvider();
return (
<CartContext.Provider value={cartValue}>{children}</CartContext.Provider>
);
};
export const useCart = () => {
return useContext(CartContext);
};
export const useCartProvider = () => {
const initialValue = [{name: name, price: price, amount: amount}];
const [cartAmount, setCartAmount] = useState(initialValue);
const addToCart = (name, price, amount) => {
setCartAmount([
...cartAmount,
{ name: name, price: price, amount: amount },
]);
};
return {
cartAmount,
setCartAmount,
addToCart,
};
};
When I add something to the Cart, I want to display the current amount of products in the cart with a little number above the cart. I implemented everything to the point that I see the number and can access the state variable cartAmount in the Navbar component.
Unfortunately it does not update. When I add to the cart I can console.log the event but the badge above the cart is not being updated.
Also - the update does not happen in real time.
I've read about Reacts ability to badge state updates for performance reasons and I also read about using useReducer hook for the actions.
With my current knowledge I feel like I cannot make the connection between all of these elements, yet.
Would like your help on that - please let me know if you need more information than provided.
Thanks in advance.

Related

Access custom hook after conditions

I'm making a Quiz app with react and typescript.
I have created a quiz context provider to wrap the functionality and pass it to the children.
The value inside my quiz provider is presented with a custom hook called useQuiz that handles all of my game logic - receives (category from url-params, questions from the backend) and returns useful data and methods to play it effectively.
Unfortunately, because of my pre-made custom hook, I can't wait for the questions data to fetch and render {children} as a result. When I'll add conditions to my jsx (for instance, display standby screen while waiting), react rules of hooks will be broken.
However, if I would write useQuiz logic inside the provider, it will fix my problem. but the structure might be messy for reading.
In my code example, react first render the page with questions marked as undefined. To overcome the error, I have added questionsJson file to be the default questions before fetching (just for demonstration purposes).
I'd like some help to still use useQuiz in my context provider and render a loading page, without breaking react rules. Alternatively, I would be glad to hear other suggestions or patterns.
Down below I added the code referenced to my explanation.
Any help will be appreciated :)
QuizProvider :
import { createContext, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { Question, Provider } from '../types';
import useQuiz from '../hooks/useQuiz';
import questionsJson from '../lib/questions.json';
import useFetch from '../hooks/useFetch';
export const QuizContext = createContext({} as ReturnType<typeof useQuiz>);
export default function _QuizProvider({ children }: Provider) {
const { pathname } = useLocation();
const { category } = useParams();
const { fetchData: fetchQuestions, data: questions, loading, error } = useFetch<Question[]>();
useEffect(() => {
fetchQuestions(`${pathname}/questions`, 'GET');
} , []);
return (
<QuizContext.Provider value={useQuiz({ category: category!, questions: questions || questionsJson.geography })}>
{children}
</QuizContext.Provider>
);
}
EDIT
useQuiz :
import { useState } from 'react';
import { Answer, Quiz, useQuiz } from '../types';
export default function _useQuiz({ category, questions }: useQuiz): Quiz {
const QUESTIONS_TIMER = 10 * 60;
const [questionNo, setQuestionNo] = useState(1);
const [answers, setAnswers] = useState(new Array<Answer>(questions.length));
const [finish, setFinish] = useState(false);
return {
category,
questions,
timer: QUESTIONS_TIMER,
questionNo,
totalQuestions: questions.length,
currentQuestion: questions[questionNo - 1],
finish,
nextQuestion: () => setQuestionNo(questionNo + 1),
previousQuestion: () => setQuestionNo(questionNo - 1),
toggleQuestion: (index: number) => setQuestionNo(index),
onAnswer: (answer: Answer) => {
const newAnswers = [...answers];
newAnswers[questionNo - 1] = answer;
setAnswers(newAnswers);
},
isSelected: (answer: Answer) =>
JSON.stringify(answers[questionNo - 1]) === JSON.stringify(answer),
isAnswersMarked: () =>
answers.every(answer => answer !== undefined),
finishQuiz: () => setFinish(true),
score: () =>
answers.filter(answer => answer && answer.correct).length
};
}
I realize that my structure is illegal.
I tried to render a custom hook after several condition which breaks react rules.
So, there are two pattern that could solve my issue:
Replacing the custom hook with a component.
Replacing the custom hook with a reducer function. That’s a perfect fit for me.
I hope it’ll help you :)

Filter item on item list that is on state using useState and useEffect

I have a list of items on my application and I am trying to create a details page to each one on click. Moreover I am not managing how to do it with useState and useEffect with typescript, I could manage just using componentDidMount and is not my goal here.
On my state, called MetricState, I have "metrics" and I have seen in some places that I should add a "selectedMetric" to my state too. I think this is a weird solution since I just want to be able to call a dispatch with the action of "select metric with id x on the metrics list that is on state".
Here is the codesandbox, when you click on the "details" button you will see that is not printing the name of the selected metric coming from the state: https://codesandbox.io/s/react-listing-forked-6sd99
This is my State:
export type MetricState = {
metrics: IMetric[];
};
My actionCreators.js file that the dispatch calls
export function fetchMetric(catalog, metricId) {
const action = {
type: ACTION_TYPES.CHOOSE_METRIC,
payload: []
};
return fetchMetricCall(action, catalog, metricId);
}
const fetchMetricCall = (action, catalog, metricId) => async (dispatch) => {
dispatch({
type: action.type,
payload: { metrics: [catalog.metrics.filter((x) => x.name === metricId)] } //contains the data to be passed to reducer
});
};
and finally the MetricsDeatail.tsx page where I try to filter the selected item with the id coming from route parametes:
const metricId = props.match.params.metricId;
const catalog = useSelector<RootState, MetricState>(
(state) => state.fetchMetrics
);
const selectedMetric: IMetric = {
name: "",
owner: { team: "" }
};
const dispatch = useDispatch();
useEffect(() => {
dispatch(appReducer.actionCreators.fetchMetric(catalog, metricId));
}, []);
On my state, called MetricState, I have "metrics" and I have seen in some places that I should add a "selectedMetric" to my state too. I think this is a weird solution since I just want to be able to call a dispatch with the action of "select metric with id x on the metrics list that is on state".
I agree with you. The metricId comes from the URL through props so it does not also need to be in the state. You want to have a "single source of truth" for each piece of data.
In order to use this design pattern, your Redux store needs to be able to:
Fetch and store a single Metric based on its id.
Select a select Metric from the store by its id.
I generally find that to be easier with a keyed object state (Record<string, IMetric>), but an array works too.
Your component should look something like this:
const MetricDetailsPage: React.FC<MatchProps> = (props) => {
const metricId = props.match.params.metricId;
// just select the one metric that we want
// it might be undefined initially
const selectedMetric = useSelector<RootState, IMetric | undefined>(
(state) => state.fetchMetrics.metrics.find(metric => metric.name === metricId )
);
const dispatch = useDispatch();
useEffect(() => {
dispatch(appReducer.actionCreators.fetchMetric(metricId));
}, []);
return (
<div>
<Link to={`/`}>Go back to main page</Link>
Here should print the name of the selected metric:{" "}
<strong>{selectedMetric?.name}</strong>
</div>
);
};
export default MetricDetailsPage;
But you should re-work your action creators so that you aren't selecting the whole catalog in the component and then passing it as an argument to the action creator.

Adding Redux State Values

I'm trying to combine two separate prices into one total price. I have the two individual price states stored and correctly updating using independent reducers, but I need a way to combine these two dynamic values into a third, total price state.
import React, {useState} from "react";
import { useSelector, useDispatch } from "react-redux";
const planPrice = useSelector(state => state.planPrice);
const repairPrice = useSelector(state => state.repairPrice);
//Use the state hook to store the Total Price
const [totalPrice, setTotalPrice] = useState(0);
const [items, setItems] = useState([]);
function addItem (newItem) {
setItems((prevItems) => {
return [...prevItems, newItem];
});
//Update the Plan Price
if ({plan}.plan === "Premium") {
dispatch(addItemPremium());
} else if ({plan}.plan === "Basic") {
dispatch(addItemBasic());
}
//Update the Repair Price
if (newItem === "Small Chip") {
dispatch(addSmallChip());
} else if (newItem === "Big Chip") {
dispatch(addBigChip());
}
// //Update the Total Price
setTotalPrice({planPrice} + {repairPrice});
}
Is this even possible using Redux, or am I going about this the wrong way?
If you have derived state, you should consider using selectors. These are functions that take state as an argument and return a derived state value.
For example:
function totalPrice(state) {
return state.price1 + state.price2;
}
If you need to use this in mapStateToProps, you can do so as follows:
const mapStateToProps = state => ({
price: totalPrice(state)
})
If you're using hooks, you can use the useSelector hook:
const price = useSelector(totalPrice);
There are libraries (e.g., reselect) that will help you create composable selectors with caching for efficiency.
Note that the reason you should use selectors versus storing redundant state, as you may have intuited, is that redundant state can get out of sync and then you're hosed. But if you have a function that computes the total price, it'll always be in sync with the individual state parts.

React native moving large code into separate file - is it a hook?

I have a react native screen that has a very long code that I would like to refractor.
Say my screen.jsx is (simplified, of course):
import React, { useState, useCallback, useEffect } from 'react';
import useLocation from '../hooks/useLocation'; // A custom hook I wrote. This one makes sense to use as a hook. It's a function that returns a location.
...
export default function Screen() {
const [fetchingLocation, region, setRegion] = useLocation();
// FROM HERE DOWN
const [fetchingRestaurants, setFetchingRestaurants] = useState(false);
const [restaurants, setRestaurants] = useState([]);
const [errorMessage, setErrorMessage] = useState('');
const initSearch = useCallback(async ({ searchQuery, region }) => {
setFetchingRestaurants(true);
try {
const response = await remoteApi.get('/search', {
params: {
term: searchQuery,
latitude: region.latitude,
longitude: region.longitude,
},
});
const fetchedRestaurants = response.data.businesses;
const fetchedRestaurantsArray = fetchedRestaurants.map((restaurant) => ({
id: restaurant.id,
name: restaurant.name,
}));
setRestaurants(fetchedRestaurantsArray);
setFetchingRestaurants(false);
} catch (e) {
setRestaurants([]);
setFetchingRestaurants(false);
}
}, []);
return (
<View>...</View>
);
}
To better structure my code, I would like to move all the code you see below "FROM HERE DOWN" (initSearch as well as the three state management useState hooks above it) into another file and import it.
At the moment I created a custom useRestaurantSearch hook in the hooks folder like so:
export default function useRestaurantSearch() {
// The code I mentioned goes here
return [initSearch, errorMessage, restaurants, setRestaurants, fetchingRestaurants];
}
Then in my Screen.jsx file I import it import useRestaurantSearch from '../hooks/useRestaurantSearch'; and inside function Screen() I grab the consts I need with
const [
initSearch,
errorMessage,
restaurants,
setRestaurants,
fetchingRestaurants,
] = useRestaurantSearch();
This works, but I feel like it can be better written and this whole approach seems weird - is it really a custom hook? If it's not a custom hook, does it belong in a util folder as a utility?
How would you approach this?
Yes this would be considered a custom hook since according to the React docs, custom hooks are just a mechanism to reuse stateful logic.
One thing that could help simplify it is:
using a library like TanStack Query (formerly React Query). You could create a query to fetch the restaurants and then you could use the data and fetchStatus from the query instead of adding them to state.

Component not re rendering when value from useContext is updated

I'm using React's context api to store an array of items. There is a component that has access to this array via useContext() and displays the length of the array. There is another component with access to the function to update this array via useContext as well. When an item is added to the array, the component does not re-render to reflect the new length of the array. When I navigate to another page in the app, the component re-renders and reflects the current length of the array. I need the component to re-render whenever the array in context changes.
I have tried using Context.Consumer instead of useContext but it still wouldn't re-render when the array was changed.
//orderContext.js//
import React, { createContext, useState } from "react"
const OrderContext = createContext({
addToOrder: () => {},
products: [],
})
const OrderProvider = ({ children }) => {
const [products, setProducts] = useState([])
const addToOrder = (idToAdd, quantityToAdd = 1) => {
let newProducts = products
newProducts[newProducts.length] = {
id: idToAdd,
quantity: quantityToAdd,
}
setProducts(newProducts)
}
return (
<OrderContext.Provider
value={{
addToOrder,
products,
}}
>
{children}
</OrderContext.Provider>
)
}
export default OrderContext
export { OrderProvider }
//addToCartButton.js//
import React, { useContext } from "react"
import OrderContext from "../../../context/orderContext"
export default ({ price, productId }) => {
const { addToOrder } = useContext(OrderContext)
return (
<button onClick={() => addToOrder(productId, 1)}>
<span>${price}</span>
</button>
)
}
//cart.js//
import React, { useContext, useState, useEffect } from "react"
import OrderContext from "../../context/orderContext"
export default () => {
const { products } = useContext(OrderContext)
return <span>{products.length}</span>
}
//gatsby-browser.js//
import React from "react"
import { OrderProvider } from "./src/context/orderContext"
export const wrapRootElement = ({ element }) => (
<OrderProvider>{element}</OrderProvider>
)
I would expect that the cart component would display the new length of the array when the array is updated, but instead it remains the same until the component is re-rendered when I navigate to another page. I need it to re-render every time the array in context is updated.
The issue is likely that you're mutating the array (rather than setting a new array) so React sees the array as the same using shallow equality.
Changing your addOrder method to assign a new array should fix this issue:
const addToOrder = (idToAdd, quantityToAdd = 1) =>
setProducts([
...products,
{
id: idToAdd,
quantity: quantityToAdd
}
]);
As #skovy said, there are more elegant solutions based on his answer if you want to change the original array.
setProducts(prevState => {
prevState[0].id = newId
return [...prevState]
})
#skovy's description helped me understand why my component was not re-rendering.
In my case I had a provider that held a large dictionary and everytime I updated that dictionary no re-renders would happen.
ex:
const [var, setVar] = useState({some large dictionary});
...mutate same dictionary
setVar(var) //this would not cause re-render
setVar({...var}) // this caused a re-render because it is a new object
I would be weary about doing this on large applications because the re-renders will cause major performance issues. in my case it is a small two page applet so some wasteful re-renders are ok for me.
Make sure to do the object or array to change every property.
In my examples, I updated the context from another file, and I got that context from another file also.
See my 'saveUserInfo' method, that is heart of the whole logic.

Resources