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
Related
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 ");
}
}
};
Hello I am using a switch statement to serve particular components to a page In my next js project. The switch statement receives a payload which it loops through in order to derive what component to serve. These components have been imported dynamically and I now wish to use this dynamic importing along with the Intersection Observer to load components when they come in the viewport to decrease the Initial page load time and split up the chunks. I have incorporated a hook that uses the intersection observer along with use ref to try to replicate my idea. Now this works when I give the reference to one div and it observes the component coming into the viewport as expected, however when I add multiple refs to my divs, I still only get the one div being observed with the ref.
What am I doing wrong? I thought you could reference the same ref multiple times and just use .current to identify the current element being observed?
Switch Statement:
import React from 'react';
import getTCTEnv from '../../../lib/helpers/get-tct-env';
import IconWishlistButton from '../../wishlist/add-to-wishlist-button/button-types/icon-wishlist-button';
import loadable from '#loadable/component';
import { useOnScreen } from '../../../hooks/on-screen';
const PriorityCollection = loadable(
() => import('#culture-trip/tile-ui-module/dist/collectionRail/PriorityCollections'),
{
resolveComponent: (components) => components.PriorityCollection
}
);
const TravelWithUs = loadable(
() => import('../../../components/trips/travel-with-us/travel-with-us'),
{
resolveComponent: (components) => components.TravelWithUs
}
);
const TrustMessaging = loadable(() => import('../../../components/trips/trust-messaging/index'), {
resolveComponent: (components) => components.TrustMessaging
});
const PressMessaging = loadable(() => import('../../../components/trips/press-messaging'), {
resolveComponent: (components) => components.PressMessaging
});
const TripsChatBanner = loadable(
() => import('../../../components/trips/chat-banner/chat-banner'),
{
resolveComponent: (components) => components.TripsChatBanner
}
);
const HpFeaturedArticles = loadable(
() => import('../home-page-featured-articles/home-page-featured-articles'),
{
resolveComponent: (components) => components.HpFeaturedArticles
}
);
const InstagramSection = loadable(() => import('../../../components/trips/instagram'), {
resolveComponent: (components) => components.InstagramSection
});
const EmailForm = loadable(() => import('../../../components/trips/email-form'));
const ReviewsSection = loadable(() => import('../../../components/trips/reviews'));
export const IncludeComponent = ({ collections, reviewData, type }) => {
const [containerRef, isVisible] = useOnScreen({
root: null,
rootMargin: '0px',
threshold: 0.1
});
const instagramCollection = collections.filter((collection) => collection.type === 'instagram');
const getComponents = () =>
collections.map((el, i) => {
switch (el.type) {
case 'trips':
case 'article':
return (
<PriorityCollection
key={i}
collections={[el]}
tctEnv={getTCTEnv()}
wishlistButton={<IconWishlistButton />}
/>
);
case 'reviews':
return (
<>
<div ref={containerRef} id={i}></div>
<ReviewsSection reviewData={reviewData} />
</>
);
case 'instagram':
return (
<>
<div ref={containerRef} id={i}></div>
<InstagramSection collection={instagramCollection} />
</>
);
case 'featured':
return <PressMessaging />;
case 'trust':
return <TrustMessaging type={type} />;
case 'featuredArticle':
return <HpFeaturedArticles />;
case 'email':
return <EmailForm />;
case 'chat':
return <TripsChatBanner />;
case 'travel':
return <TravelWithUs type={type} />;
default:
return;
}
});
return getComponents();
};
custom hook:
import { useEffect, useState, useRef } from 'react';
export const useOnScreen = (options): any => {
const containerRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState([]);
const callbackFunction = (entries) => {
const [entry] = entries;
if (entry.isIntersecting)
setIsVisible((oldArray) => [
...oldArray,
isVisible.indexOf(entry.target.id) === -1 && entry.target.id !== undefined
? entry.target.id
: console.log('nothing')
]);
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current) observer.observe(containerRef.current);
return () => {
if (containerRef.current) observer.unobserve(containerRef.current);
};
}, [containerRef.current, options]);
return [containerRef, isVisible];
};
Currently only the instagram ref gets observed
If I understand your code correctly, more than one component is possibly rendered from getComponents.
For instance, the tree could contain:
<div ref={containerRef} id={i}></div>
<ReviewsSection reviewData={reviewData} />
<div ref={containerRef} id={i}></div>
<InstagramSection collection={instagramCollection} />
And you want both divs there to be observed.
It doesn't work because the ref doesn't trigger the effect by itself. The ref is simply an object like { current: null }.
When the tree is rendered, containerRef.current is set to the first div, then it is set to the second div, then the effect runs.
To do what you want you can:
Call the custom hook multiple times, and assign one containerRef to each div. The issue here is, of course, you will also have multiple IntersectionObservers instances.
Declare multiple refs and pass them to the custom hook via argument, instead of returning the ref from the custom hook.
Implement a callback ref that adds every div to a list, skipping duplicates. This one allows you to keep the same implementation in getComponents, but is also the trickiest for the hook.
Solved with this:
import React, { useEffect, useReducer } from 'react';
import getTCTEnv from '../../../lib/helpers/get-tct-env';
import IconWishlistButton from '../../wishlist/add-to-wishlist-button/button-types/icon-wishlist-button';
import loadable from '#loadable/component';
import { useOnScreen } from '../../../hooks/on-screen';
const PriorityCollection = loadable(
() => import('#culture-trip/tile-ui-module/dist/collectionRail/PriorityCollections'),
{
resolveComponent: (components) => components.PriorityCollection
}
);
const TravelWithUs = loadable(
() => import('../../../components/trips/travel-with-us/travel-with-us'),
{
resolveComponent: (components) => components.TravelWithUs
}
);
const TrustMessaging = loadable(() => import('../../../components/trips/trust-messaging/index'), {
resolveComponent: (components) => components.TrustMessaging
});
const PressMessaging = loadable(() => import('../../../components/trips/press-messaging'), {
resolveComponent: (components) => components.PressMessaging
});
const TripsChatBanner = loadable(
() => import('../../../components/trips/chat-banner/chat-banner'),
{
resolveComponent: (components) => components.TripsChatBanner
}
);
const HpFeaturedArticles = loadable(
() => import('../home-page-featured-articles/home-page-featured-articles'),
{
resolveComponent: (components) => components.HpFeaturedArticles
}
);
const InstagramSection = loadable(() => import('../../../components/trips/instagram'), {
resolveComponent: (components) => components.InstagramSection
});
const EmailForm = loadable(() => import('../../../components/trips/email-form'));
const ReviewsSection = loadable(() => import('../../../components/trips/reviews'));
export const IncludeComponent = ({ collections, reviewData, type }) => {
const [containerRef, isVisible] = useOnScreen({
root: null,
rootMargin: '0px',
threshold: 0.1
});
const instagramCollection = collections.filter((collection) => collection.type === 'instagram');
const getComponents = () =>
collections.map((el, i) => {
switch (el.type) {
case 'trips':
case 'article':
return (
<PriorityCollection
key={i}
collections={[el]}
tctEnv={getTCTEnv()}
wishlistButton={<IconWishlistButton />}
/>
);
case 'reviews':
return (
<>
<div
ref={(element) => {
containerRef.current[i] = element;
}}
id={i}
></div>
{isVisible.indexOf(i.toString()) !== -1 && <ReviewsSection reviewData={reviewData} />}
</>
);
case 'instagram':
return (
<>
<div
ref={(element) => {
containerRef.current[i] = element;
}}
id={i}
></div>
<InstagramSection collection={instagramCollection} />
</>
);
case 'featured':
return <PressMessaging />;
case 'trust':
return <TrustMessaging type={type} />;
case 'featuredArticle':
return <HpFeaturedArticles />;
case 'email':
return <EmailForm />;
case 'chat':
return <TripsChatBanner />;
case 'travel':
return <TravelWithUs type={type} />;
default:
return;
}
});
return getComponents();
};
hook:
import { useEffect, useState, useRef } from 'react';
export const useOnScreen = (options): any => {
const containerRef = useRef<HTMLDivElement[]>([]);
const [isVisible, setIsVisible] = useState([]);
const callbackFunction = (entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
const checkIdInArray = isVisible.indexOf(entry.target.id) === -1;
if (checkIdInArray) setIsVisible((oldArray) => [...oldArray, entry.target.id]);
}
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current)
containerRef.current.forEach((el) => {
observer.observe(el);
});
return () => {
if (containerRef.current)
containerRef.current.forEach((el) => {
observer.unobserve(el);
});
};
}, [containerRef, options]);
return [containerRef, isVisible];
};
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
});
}, []);
What I want is to paginate my data but the problem is when I'm searching for specific data if I'm on page 3 the result shows on page 1 always and I can't see anything because I was on page no 3. I want to go to page 1 automatically when I'm searching for something. Also when I press the next button if there is no data at all it still increases the page number.
Here is my code:
import { React, useState, useEffect } from "react";
import UpdateDialogue from "./UpdateDialogue";
function List(props) {
const API_URL = "http://dummy.restapiexample.com/api/v1/employees";
const [EmployeeData, setEmployeeData] = useState([]);
const [pageNumber, setPageNumber] = useState(1);
const [postNumber] = useState(8);
const currentPageNumber = pageNumber * postNumber - postNumber;
const handlePrev = () => {
if (pageNumber === 1) return;
setPageNumber(pageNumber - 1);
};
const handleNext = () => {
setPageNumber(pageNumber + 1);
};
useEffect(() => {
fetch(API_URL)
.then((response) => response.json())
.then((response) => {
setEmployeeData(response.data);
})
.catch((err) => {
console.error(err);
});
}, []);
const filteredData = EmployeeData.filter((el) => {
if (props.input === "") {
return el;
} else {
return el.employee_name.toLowerCase().includes(props.input)
}
});
const paginatedData = filteredData.splice(currentPageNumber, postNumber);
return (
<>
<ul>
{paginatedData.map((user) => (
<UpdateDialogue user={user} key={user.id} />
))}
</ul>
<div>Page {pageNumber} </div>
<div>
<button style={{marginRight:10}} onClick={handlePrev}>prev</button>
<button onClick={handleNext}>next</button>
</div>
</>
);
}
export default List;
Maybe with a useEffect on your input:
useEffect(() => {
if (props.input) {
setPageNumber(1);
}
}, [props.input]);
That way, whenever your input changes, your page number is set to 1.
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>
);
}