Why is my hooks states going back to its initial states? - javascript

I've been making a game which at the end, requires the user to type their guess. To avoid confusion in my actual project, I created something in codesandbox which demonstrates the problem I'm having. I should add that the game in codesandbox isn't suppose to make much sense. But essentially you just click any box 5 times which generates a random number and when the component mounts, it also creates an array with 5 random number. At the end, you type a number and it checks if both arrays contain the key entered and colors them accordingly. The problem I'm having is that once the guess component is shown, all the hooks states return to their initial states.
Main.tsx
import { Guess } from "./Guess";
import { useHook } from "./Hook";
import { Loading } from "./Loading";
import "./styles.css";
export const Main = () => {
const {loading, count, handleClick, randArr} = useHook()
return (
<div className="main">
{!loading && count < 5 &&
<div className='click-container'>
{Array.from({length: 5}).fill('').map((_, i: number) =>
<div onClick={handleClick} className='box' key={i}>Click</div>
)}
</div>
}
{loading && <Loading count={count} />}
{!loading && count >= 5 && <Guess arr={randArr} />}
</div>
);
}
Hook.tsx
import { useEffect, useState } from 'react'
export const useHook = () => {
type guessType = {
keyNum: number
isContain: boolean
}
const [disable, setDisable] = useState(true)
const [randArr, setRandArr] = useState<number[]>([])
const [initialArr, setInitialArr] = useState<number[]>([])
const [count, setCount] = useState<number>(0)
const [loading, setLoading] = useState(true)
const [guess, setGuess] = useState<guessType[]>([])
const randomNum = () => {
return Math.floor(Math.random() * (9 - 0 + 1) + 0);
}
useEffect(() => {
const handleInitialArr = () => {
for (let i = 0; i < 5; i++) {
let num = randomNum()
setInitialArr((prev) => [...prev, num])
}
}
handleInitialArr()
}, [])
const handleClick = () => {
if (!disable) {
let num = randomNum()
setRandArr((prev)=> [...prev, num])
setCount((prev) => prev + 1)
setDisable(true)
setLoading(true)
}
}
useEffect(()=> {
const handleLoading = () => {
setTimeout(() => {
setLoading(false)
}, 500)
}
const handleRound = () => {
setDisable(false)
}
handleLoading()
handleRound()
}, [count])
const handleKeyUp = ({key}) => {
const isNumber = /^[0-9]$/i.test(key)
if (isNumber) {
if (randArr.includes(key) && initialArr.includes(key)) {
setGuess((prev) => [...prev, {keyNum: key, isContain: true}])
console.log(' they both have this number')
} else {
setGuess((prev) => [...prev, {keyNum: key, isContain: false}])
console.log(' they both do not contain this number ')
}
}
}
console.log(count)
console.log(randArr, ' this is rand arr')
console.log(initialArr, ' this is initial arr')
return {
count,
loading,
handleClick,
randArr,
handleKeyUp,
guess
}
}
Guess.tsx
import React, { useEffect } from "react";
import { useHook } from "./Hook";
import "./styles.css";
type props = {
arr: number[];
};
export const Guess: React.FC<props> = (props) => {
const { handleKeyUp, guess } = useHook();
useEffect(() => {
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keyup", handleKeyUp);
};
}, [handleKeyUp]);
console.log(props.arr, " this is props arr ");
return (
<div className="content">
<div>
<p>Guesses: </p>
<div className="guess-list">
{guess.map((item: any, i: number) =>
<p key={i} className={guess[i].isContain ? 'guess-num-true': 'guess-num-false'} >{item.keyNum}</p>
)}
</div>
</div>
</div>
);
};
Also, here is the codesandbox if you want to take a look for yourself: https://codesandbox.io/s/guess-numbers-70fss9
Any help would be deeply appreciated!!!

Fixed demo: https://codesandbox.io/s/guess-numbers-fixed-kz3qmw?file=/src/my-context.tsx:1582-2047
You're under the misconception that hooks share state across components. The hook will have a new state for every call of useHook(). To share state you need to use a Context.
type guessType = {
keyNum: number;
isContain: boolean;
};
type MyContextType = {
count: number;
loading: boolean;
handleClick: () => void;
randArr: number[];
handleKeyUp: ({ key: string }) => void;
guess: guessType[];
};
export const MyContext = createContext<MyContextType>(null as any);
export const MyContextProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
// Same stuff as your hook goes here
return (
<MyContext.Provider
value={{ count, loading, handleClick, randArr, handleKeyUp, guess }}
>
{children}
</MyContext.Provider>
);
};
export const App = () => {
return (
<div className="App">
<MyContextProvider>
<Page />
</MyContextProvider>
</div>
);
};
export const Main = () => {
const { loading, count, handleClick, randArr } = useContext(MyContext);
...
}
export const Guess: React.FC<props> = (props) => {
const { handleKeyUp, guess } = useContext(MyContext);
...
}
Your handleKeyUp function is also bugged, a good example of why you need to type your parameters. key is a string, not a number. So the condition will always be false.
const handleKeyUp = ({ key }: {key: string}) => {
const num = parseInt(key);
if (!isNaN(num)) {
if (randArr.includes(num) && initialArr.includes(num)) {
setGuess((prev) => [...prev, { keyNum: num, isContain: true }]);
console.log(" they both have this number");
} else {
setGuess((prev) => [...prev, { keyNum: num, isContain: false }]);
console.log(" they both do not contain this number ");
}
}
};

Related

usage of React.memo() inside components with prop functions

import React, { useState } from 'react'
const App = () => {
const [count, setCount] = useState<number>(0);
const [otherCount, setOtherCount] = useState<number>(0);
const increment = () => {
setCount((pre) => {
return pre + 1
})
}
const decrease = () => {
setOtherCount((pre) => {
return pre - 1
})
}
return (
<>
<DecrementComponent decrease={decrease} />
<br />
<br />
<IncrementComponent increment={increment} />
</>
)
}
const DecrementComponent = React.memo(({ decrease }: { decrease: () => void; }) => {
console.log("DecrementComponent");
return (
<div>
<button onClick={decrease}>Decrement</button>
</div>
)
})
const IncrementComponent = React.memo(({ increment }: { increment: () => void; }) => {
console.log("IncrementComponent");
return (
<div>
<button onClick={increment}>Increment</button>
</div>
)
})
export default App
**React.memo(), although I used React.memo(), when I clicked increment or decrement functions, two components were rendered.
But I think one component shoud be rendered in this senerio. Why were two component rendered ?
**
React.memo can only help if the props don't change. But the increment and decrement functions change on every render, so the props are always changing. You will need to memoize those functions so that they don't change.
const increment = useCallback(() => {
setCount((pre) => {
return pre + 1
});
}, []);
const decrement = useCallback(() => {
setCount((pre) => {
return pre - 1
});
}, []);

How to prevent my Page render when I click on the button

I am trying to optimize my react application, while profiling my application I found that when I click on Add to cart page my whole page is getting re-rendered. Could anyone help me with, how to avoid that and why it is happening?
FYR, GitHub repo:https://github.com/sandeep8080/shopping-cart-assignment
import { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import SideBar from "../../components/sideBar/SideBar";
import { getProductsData } from "../../redux/action/products";
import ProductCard from '../../components/productCard/ProductCard';
import './products.css';
import { getCategoryData } from "../../redux/action/category";
import Cart from "../cart/cart";
import Modal from '../../components/modal/Modal';
import { useHistory, useParams } from "react-router";
const ProductsPage = () => {
const dispatch = useDispatch();
const router = useHistory();
const { id } = useParams();
console.log(` product comp : ${id}`);
const productsData = useSelector(data => data.Products.products);
const sideBarData = useSelector(data => {
const listItems = data.Categories.CategoriesItems;
const activeListItems = listItems.filter(item => item.enabled === true);
return activeListItems;
});
const openCart = useSelector(state => state.CartDetails.isOpen);
const [fProductData, setFProductData] = useState([]);
useEffect(() => {
dispatch(getProductsData());
dispatch(getCategoryData());
}, []);
useEffect(() => {
if (id) {
filterDataByCategory(id);
} else {
setFProductData(productsData);
}
}, [productsData, id]);
// Function to filter out the data based on category
const filterDataByCategory = (id) => {
console.log("Filter data function called")
const filterData = productsData.filter(item => item.category === id);
setFProductData(filterData);
};
const handleClickProduct = useCallback((id) => {
filterDataByCategory(id);
router.push(`/products/${id}`);
}, [id]);
return (
<div className='product-main'>
<SideBar
sideBarData={sideBarData}
handleClickProduct={handleClickProduct}
/>
<div className='product-container'>
<div className='product-row'>
{
(fProductData).map((product) => {
return (
<div key={product.id} className='card-wrapper' >
<ProductCard key={product.id} {...product} />
</div>
)
})
}
</div>
</div>
{
openCart &&
<Modal>
<Cart />
</Modal>
}
</div >
)
};
export default ProductsPage;
// Product Card component
import './ProductCard.css';
import Button from '../button/Button';
import React from 'react';
import { useDispatch } from 'react-redux';
import { updateCart } from '../../redux/action/cart';
import priceFromatter from '../../lib/priceFromatter';
const ProductCard = ({ name, price, description, imageURL, id }) => {
const dispatch = useDispatch();
const handleClick = () => {
console.log('product clicked', id);
dispatch(updateCart(id, 'add'));
};
let imgURL = `../../${imageURL}`;
// imgURL = imgURL.replace(/([^:]\/)\/+/g, "$1");
// const image = React.lazy(() => import (`${imgURL}`));
// console.log(image);
return (
<article className='card-container'>
<h6 className='card-header'>
{name}
</h6>
<div className='content-container'>
<img
className='content-img'
// src={require(`${imgURL}`).default}
src={imageURL}
/>
<div className='content'>
<p className='content-desc'>{description}</p>
<div className='content-footer'>
<p>{priceFromatter(price)}</p>
<Button btnText='Add To Cart' handleClick={() => handleClick(id)} />
</div>
</div>
</div>
</article>
)
};
export default ProductCard;
import { callApi } from "../../lib/api";
import { actions } from '../actionContants/actionConstant';
export const toggleCart = (isToggle) => {
return {
type: actions.OPEN_CART,
payload: isToggle,
}
};
export const updateCart = (id, operation) => {
return async (dispatch, getState) => {
const productList = getState().Products.products;
const cartItems = getState().CartDetails.cartItems;
const currItem = productList.find(({ id: currentItemId }) => currentItemId === id);
const isItemInCart = cartItems.find(({ id }) => id === currItem.id);
let finalItem = [];
if (!isItemInCart) {
finalItem = [...cartItems, { ...currItem, count: 1 }]
} else {
finalItem = cartItems.map(item => {
if (item.id === currItem.id) {
operation === 'add' ? item.count = item.count + 1 : item.count = item.count - 1
}
return item;
}).filter(({ count }) => count)
}
try {
const result = await callApi.post('/addToCart', id);
result && dispatch({
type: actions.UPDATE_TO_CART,
payload: finalItem
})
} catch (error) {
console.log(error)
}
}
};
In products.js change the following block of code:
const sideBarData = useSelector(data => {
const listItems = data.Categories.CategoriesItems;
const activeListItems = listItems.filter(item => item.enabled === true);
return activeListItems;
});
to:
const sideBarData = useSelector(data => {
const listItems = data.Categories.CategoriesItems;
const activeListItems = listItems.filter(item => item.enabled === true);
return activeListItems;
}, shallowEqual);
useSelector will force a component to re-render when the selector returns a new reference that is different than the previous reference (it uses the === operator). Ref: https://react-redux.js.org/api/hooks#equality-comparisons-and-updates. As you are filtering the array returned from the store, it will always be a different object reference to the one in the store.
The use of shallowEqual as the equalityFn to useSelector() can be used to change the comparison and prevent an unnecessary re-render of the <ProductsPage> component.
did you try using e.preventDefault() otherwise the answer above might work

Is it possible to expose a function defined within a React function component to be called in other components?

I'm refactoring some old code for an alert widget and am abstracting it into its own component that uses DOM portals and conditional rendering. I want to keep as much of the work inside of this component as I possibly can, so ideally I'd love to be able to expose the Alert component itself as well as a function defined inside of that component triggers the render state and style animations so that no outside state management is required. Something like this is what I'm looking to do:
import Alert, { renderAlert } from '../Alert'
const CopyButton = () => (
<>
<Alert text="Text copied!" />
<button onClick={() => renderAlert()}>Copy Your Text</button>
</>
)
Here's what I currently have for the Alert component - right now it takes in a state variable from outside that just flips when the button is clicked and triggers the useEffect inside of the Alert to trigger the renderAlert function. I'd love to just expose renderAlert directly from the component so I can call it without the additional state variable like above.
const Alert = ({ label, color, stateTrigger }) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger])
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
};
export default Alert;
Though it's not common to "reach" into other components and invoke functions, React does allow a "backdoor" to do so.
useImperativeHandle
React.forwardRef
The idea is to expose out the renderAlert function imperatively via the React ref system.
Example:
import { forwardRef, useImperativeHandle } from 'react';
const Alert = forwardRef(({ label, color, stateTrigger }, ref) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger]);
useImperativeHandle(ref, () => ({
renderAlert,
}));
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
});
export default Alert;
...
import { useRef } from 'react';
import Alert from '../Alert'
const CopyButton = () => {
const ref = useRef();
const clickHandler = () => {
ref.current?.renderAlert();
};
return (
<>
<Alert ref={ref} text="Text copied!" />
<button onClick={clickHandler}>Copy Your Text</button>
</>
)
};
A more React-way to accomplish this might be to abstract the Alert state into an AlertProvider that renders the portal and handles the rendering of the alert and provides the renderAlert function via the context.
Example:
import { createContext, useContext, useState } from "react";
interface I_Alert {
renderAlert: (text: string) => void;
}
const AlertContext = createContext<I_Alert>({
renderAlert: () => {}
});
const useAlert = () => useContext(AlertContext);
const AlertProvider = ({ children }: { children: React.ReactElement }) => {
const [text, setText] = useState<string>("");
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
...
const renderAlert = (text: string): void => {
setAlertRendered(false);
setAlertVisible(false);
setText(text);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
const ele = <div>{alertRendered && <div> ..... </div>}</div>;
return (
<AlertContext.Provider value={{ renderAlert }}>
{children}
// ... portal ...
</AlertContext.Provider>
);
};
...
const CopyButton = () => {
const { renderAlert } = useAlert();
const clickHandler = () => {
renderAlert("Text copied!");
};
return (
<>
<button onClick={clickHandler}>Copy Your Text</button>
</>
);
};
...
function App() {
return (
<AlertProvider>
...
<div className="App">
...
<CopyButton />
...
</div>
...
</AlertProvider>
);
}

Why are the elements from a mapping function not rendering?

Summarize the problem
I have a page within a Gatsby JS site that accepts state via a provider, and some of that activity is able to be used, however, I am unable to provide the contents from a mapping function that is given via context.
Expected result: the expected elements from the mapping function would render
Actual result: the elements in question are not rendered
No error messages
Describe what you've tried
I thought the issue was not explicitly entering in return on the arrow function in question, but that does not change any of the output
Also, rather than try to access the method directly on the page (via a context provider) I moved the method directly into the Provider hook. This did not change any of the rendering.
Show some code
here is Provider.js
import React, { useState, useEffect } from 'react';
import he from 'he';
export const myContext = React.createContext();
const Provider = props => {
const [state, setState] = useState({
loading: true,
error: false,
data: [],
});
const [page, setPage] = useState(1);
const [score, setScore] = useState(0);
const [correctAnswers, setCorrectAnswers] = useState([]);
const [allQuestions, setAllQuestions] = useState([]);
const [answers, setAnswers] = useState([]);
const [right, setRight] = useState([]);
const [wrong, setWrong] = useState([]);
function clearScore() {
updatedScore = 0;
}
function clearRights() {
while (rights.length > 0) {
rights.pop();
}
}
function clearWrongs() {
while (wrongs.length > 0) {
wrongs.pop();
}
}
let updatedScore = 0;
let rights = [];
let wrongs = [];
const calcScore = (x, y) => {
for (let i = 0; i < 10; i++) {
if (x[i] === y[i]) {
updatedScore = updatedScore + 1;
rights.push(i);
} else wrongs.push(i);
}
}
useEffect(() => {
fetch('https://opentdb.com/api.php?amount=10&difficulty=hard&type=boolean')
.then(response => {
return response.json()
})
.then(json => {
const correctAnswer = json.results.map(q => q['correct_answer']);
const questionBulk = json.results.map(q => q['question']);
setState({
data: json.results,
loading: false,
error: false,
});
setCorrectAnswers(correctAnswers.concat(correctAnswer));
setAllQuestions(allQuestions.concat(questionBulk));
})
.catch(err => {
setState({error: err})
})
}, [])
return (
<myContext.Provider
value={{
state, page, score, answers, right, wrong,
hitTrue: () => {setAnswers(answers.concat('True')); setPage(page + 1);},
hitFalse: () => {setAnswers(answers.concat('False')); setPage(page + 1);},
resetAll: () => {
setAnswers([]);
setPage(1);
setScore(0);
setRight([]);
setWrong([]);
clearScore();
clearWrongs();
clearRights();
},
calculateScore: () => calcScore(answers, correctAnswers),
updateScore: () => setScore(score + updatedScore),
updateRight: () => setRight(right.concat(rights)),
updateWrong: () => setWrong(wrong.concat(wrongs)),
showRightAnswers: () => {right.map((result, index) => {
return (
<p className="text-green-300 text-sm" key={index}>
+ {he.decode(`${allQuestions[result]}`)}
</p>)
})},
showWrongAnswers: () => {wrong.map((result, index) => {
return (
<p className="text-red-500 text-sm" key={index}>
- {he.decode(`${allQuestions[result]}`)}
</p>
)
})},
}}
>
{props.children}
</myContext.Provider>
);
}
export default ({ element }) => (
<Provider>
{element}
</Provider>
);
^the showRightAnswers() and showWrongAnswers() methods are the ones I am trying to figure out
and here is the results.js page.{context.showRightAnswers()} and {context.showWrongAnswers()} are where the mapped content is supposed to appear.
import React from 'react';
import Button from '../components/Button';
import { navigate } from 'gatsby';
import { myContext } from '../hooks/Provider';
const ResultsPage = () => {
return (
<myContext.Consumer>
{context => (
<>
<h1 className="">You Finished!</h1>
<p className="">Your score was {context.score}/10</p>
{context.showRightAnswers()}
{context.showWrongAnswers()}
<Button
buttonText="Try Again?"
buttonActions={() => {
context.resetAll();
navigate('/');
}}
/>
</>
)}
</myContext.Consumer>
);
}
export default ResultsPage;
You are returning inside your map, but you're not returning the map call itself - .map returns an array, and you have to return that array from your "show" functions, e.g.
showWrongAnswers: () => { return wrong.map((result, index) ...
^^^^
This will return the array .map generated from the showWrongAnswers function when it's called, and thus {context.showWrongAnswers()} will render that returned array

How to modify the code such that can call usehook using react and typescript?

A popup is displayed when the add button is clicked and the count is greater or less than 0.
below is the code,
function AddButton () {
const [isOpen, setIsOpen] = React.useState(false);
const count = useGetCount();
useTrigger(isOpen, count);
const on_add_click = () => {
setIsOpen(true);
}
return (
<button onClick={on_add_click}>add</button>
);
}
interface ContextProps {
trigger: (count: number) => void;
}
const popupContext = React.createContext<ContextProps>({
trigger: (count: number) => {},
});
const usePopupContext = () => React.useContext(popupContext);
export const popupContextProvider = ({ children }: any) => {
const [show, setShow] = React.useState(false);
const limit = 0;
const dismiss = () => {
if (show) {
sessionStorage.setItem(somePopupId, 'dismissed');
setShow(false);
}
};
const isDismissed = (dialogId: string) =>
sessionStorage.getItem(dialogId) === 'dismissed';
const context = {
trigger: (count: number) => {
if (!isDismissed(somePopupId) && count <= limit) {
setShow(true);
} else if (count > limit) {
setShow(false);
}
},
};
return (
<popupContext.Provider value={context}>
{children}
{show && (
<Popup onHide={dismiss} />
)}
</popupContext.Provider>
);
};
export function useTrigger(enabled: boolean, count: number) {
const { trigger } = usePopupContext();
React.useEffect(() => {
if (enabled) {
trigger(count);
}
}, [enabled, count, trigger]);
}
This works but calls trigger method only when enabled is true.
I want to modify the above code such that when the user clicks the add button, I want this useTrigger to happen. I don't want to check for enabled and call trigger.
I have tried the following removed checking for enabled.
export function useTrigger(enabled: boolean, count: number) {
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}, [enabled, count, trigger]);
}
this works but the popup is displayed as the count is less than or equal to 0. but I want it to first check if the add button clicked or not.
so soon after the user clicking the add button in on_add_click I want the popup to display.
How can I modify the code above? I am new to using hooks. Could someone help me with this? thanks.
EDIT:strong text
i have tried to do something like below and i get error
Uncaught Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component.
on_add_click = () => {
Trigger(count);
}
export function Trigger(count: number) {
const { trigger } = usePopupContext();
React.useEffect(() => {
trigger(count);
}, [count, trigger]);
}
how can i fix this.
If I followed you right, you just need to add a state to your hook and return the setter to call it onclick:
export function useTrigger(count: number) {
const [clicked, setClicked] = React.useState(false)
const { trigger } = usePopupContext();
React.useEffect(() => {
if(clicked) {
trigger(count);
}
}, [count, trigger, clicked]);
const clickCb = useCallback(() => {setClicked(true)}, [])
return cb
}
Then in your component with button you do something like this:
const Component = (props) => {
const onClick = useTrigger(props.count)
/* ... */
return <button onClick={onClick}/>
}

Categories