I am trying to update the individual style of each button when it is clicked, using the useRef() hook from React.
Right now, when I click any button the style change is always applied to the last button rendered.
I believe this is the bit needing attention but I'm stumped.
const handleClick = () => {
status.current.style.background = 'green';
}
Here's the full bit:
import React, { useRef } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
let background = 'blue';
let controls = [];
const makeControls = () => {
for (let i = 1; i <= 9; i++) {
controls.push({active: false});
}
return controls;
};
const ControlPanel = () => {
const status = useRef('blue');
makeControls();
const handleClick = () => {
status.current.style.background = 'green';
}
return (
<>
{controls.map((control, i) => (
<div
ref={status}
style={{background: background}}
className={'box'}
key={i}
onClick={() => handleClick()}></div>
))}
</>
);
};
ReactDOM.render(<ControlPanel />, document.getElementById('root'));
Currently, your ref targets only the last item, you should target all your control items by making an array of refs.
let controls = [];
const makeControls = () => {
for (let i = 1; i <= 9; i++) {
controls.push({ active: false });
}
return controls;
};
makeControls();
const ControlPanel = () => {
const status = useRef([]);
const handleClick = index => {
status.current[index].style.background = 'green';
};
return (
<>
{controls.map((control, i) => (
<div
ref={ref => (status.current[i] = ref)}
style={{ background: `blue`, width: 100, height: 100 }}
key={i}
onClick={() => handleClick(i)}
/>
))}
</>
);
};
When rendering the list of <div>s your status ref is getting reassigned each time, finally stopping on the last element.
which is why the last element gets updated.
Instead why not store the background color info on the control object itself
for (let i = 1; i <= 9; i++) {
controls.push({active: false,background: 'blue'});
}
{controls.map((control, i) => (
<div
style={{background: control.background}}
className={'box'}
key={i}
onClick={() => handleClick(control)}></div>
))}
const handleClick = (control) => {
control.background = 'green';
}
you can use state to do that
like this
import React, { useRef,useState } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
let controls = [];
const makeControls = () => {
for (let i = 1; i <= 9; i++) {
controls.push({active: false});
}
return controls;
};
const ControlPanel = () => {
const [controlState,setControlState]=useState({background:"blue"})
const status = useRef('blue');
makeControls();
const handleClick = () => {
setControlState({background:"green"});
}
return (
<>
{controls.map((control, i) => (
<div
ref={status}
style={{background: controlState.background}}
className={'box'}
key={i}
onClick={() => handleClick()}></div>
))}
</>
);
};
ReactDOM.render(<ControlPanel />, document.getElementById('root'));
Related
For the purposes of documenting our library, I want to convert a React Component function to JSX. For example, if we have a rendered Button component, I want to show the code for how it's constructed.
One solution could be to read Button.jsx as plain text but I feel like there should be a better solution.
// Button.jsx
export const Button = (props) => (
<button {...props}><Icon name={props.icon}/>{props.children}</button>
)
// ButtonDocs.jsx
import { Button } from 'components/Button';
const Docs = (props) => {
const renderedButton = <Button icon='home'>Hello</Button>
// Here I'd expect this function to return something like:
// `<button><Icon name="home"/>Hello</button>`
const buttonJSX = someFunctionToGetJSX(renderedButton)
return (
<div>
{renderedButton}
<code>
{buttonJSX}
</code>
</div>
)
}
Do this in Button component
const Button = (props) => {
const buttonRef = useRef();
const [jsxEl, setJsxEl] = useState("");
useEffect(() => {
let jsxArray = [];
for (let i = 0; i < buttonRef.current.children.length; i++) {
jsxArray.push(`${buttonRef.current.children[i].innerHTML}`);
}
jsxArray.join(",");
setJsxEl(jsxArray);
props.onJsxFunc(jsxEl);
}, [buttonRef]);
return (
<Fragment>
<div ref={buttonRef}>
<button {...props}>
<Icon name={props.icon} />
{props.children}
</button>
</div>
</Fragment>
);
};
export default Button;
Then in ButtonDocs component do the below.
// ButtonDocs.jsx
import { Button } from 'components/Button';
const Docs = (props) => {
const renderedButton = <Button onJsxFunc={(el) => showJsxElements(el)} icon='home'>Hello</Button>
const jsxCode = useRef();
const showJsxElements = (el) => {
jsxCode.current.innerText += `${el}`;
};
return (
<div>
{renderedButton}
<code>
<div ref={jsxCode}></div>
</code>
</div>
)
}
I am trying to get offsetTop positions of all of sections in DOM. I need it so that, I can change URL in browser when I scroll to a specific section. I can get the offsetTop position when component renders. But, when I use my components inside of react-scroll-motion, it gives me 0 as offsetTop position for all components. How can I get offsetTop position while still using react-scroll-motion library?
Below is the code with using react-scroll-motion
const Home: NextPage = () => {
const [firstRender, setFirstRender] = useState(0);
const [sections, setSections] = useState(null);
const [isBrowser, setIsBrowser] = useState(false);
useEffect(() => {
setIsBrowser(typeof window !== undefined ? true : false);
setFirstRender(1);
}, []);
useEffect(() => {
const sectionsList = document.querySelectorAll('section');
const sectionsData: any = {};
for (let i = 0; i < sectionsList.length; i++) {
console.log(sectionsList[i].offsetTop); // gives 0 when renders
sectionsData[`${sectionsList[i].id}`] = {
'offsetTop': sectionsList[i].offsetTop,
'id': sectionsList[i].id
}
}
setSections(sectionsData);
}, [firstRender]);
const contentItems = [<Hero/>, <About/>, <RoadMap/>, <Team/>];
const content = linksData.map((item, index) => {
return (
<section id={item.text} key={index}>
<ScrollPage page={item.page}>
<Animator animation={batch(item.sticky, item.fade, item.move)}>
{contentItems[index]}
</Animator>
</ScrollPage>
</section>
)
})
return (
<>
{
isBrowser &&
(
<Container>
<Navbar/>
<Main>
<MovingParticles/>
<ScrollContainer>
{content}
</ScrollContainer>
</Main>
</Container>
)
}
</>
)
}
export default Home;
Below is code that gives correct offsetTop position when I do not wrap my components inside react-scroll-motion
const Home: NextPage = () => {
const [firstRender, setFirstRender] = useState(0);
const [sections, setSections] = useState(null);
const [isBrowser, setIsBrowser] = useState(false);
useEffect(() => {
setIsBrowser(typeof window !== undefined ? true : false);
setFirstRender(1);
}, []);
useEffect(() => {
const sectionsList = document.querySelectorAll('section');
const sectionsData: any = {};
for (let i = 0; i < sectionsList.length; i++) {
console.log(sectionsList[i].scrollHeight);
sectionsData[`${sectionsList[i].id}`] = {
'offsetTop': sectionsList[i].offsetTop,
'id': sectionsList[i].id
}
}
setSections(sectionsData);
}, [firstRender]);
const contentItems = [<Hero/>, <About/>, <RoadMap/>, <Team/>];
const content = linksData.map((item, index) => {
return (
<section id={item.text} key={index}>
{contentItems[index]}
</section>
)
})
return (
<>
{
isBrowser &&
(
<Container>
<Navbar/>
<Main>
<MovingParticles/>
{content}
</Main>
</Container>
)
}
</>
)
}
export default Home;
How to get offsetTop position with using react-scroll-motion?
I'm working on a project where I mapped through a lisf of numbers from 1 to 90 and returned a button for each number, so when I click a particular button the color changes and when I click again the color goes away. So here is my problem I need to add the number in that button to a list when the button is clicked and and the color changes then remove it from the list when the button is clicked again and the color changes back to normal. This the codebase of what I did to add colors to the button when clicked and remove color when clicked again.
import React from 'react';
import './style.css';
export default function App() {
const [activeNums, setActiveNums] = React.useState({});
let nums = []
for (let i = 1; i < 91; i++) {
nums.push(i)
}
const onToggle = (num) => {
setActiveNums((state) => {
return {
...state,
[num]: !state[num],
};
});
};
return (
<div>
{nums.map(i => {
return <button key={i} name={!activeNums[i] && 'ready'} onClick={(e) =>
handleClass(i, e)} className={`${activeNums[i] ? 'game_clicked' : ''}
game_btn `}>{i}</button>
})}
</div>
);
}
So as I understand you want to have buttons, which you click/unclick and adding button number to some object. Can be done like this:
// import React from "react";
const App = () => {
const [activeNums, setActiveNums] = React.useState({});
const buttonHandler = ({ target: { name } }) => {
setActiveNums({ ...activeNums, [name]: !activeNums[name] });
};
return (
<div>
{[...Array(10)].map((_, idx) => {
const number = idx + 1;
return (
<button key={number} name={number} onClick={buttonHandler}>
My number is: {number} <br />I am{" "}
{activeNums["" + number] ? "" : "not"} ready.
</button>
);
})}
<div>{JSON.stringify(activeNums)}</div>
</div>
);
};
// export default App;
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
/* I think this is what you were going for, hope this helps - */
import React from "react";
import "./styles.css";
export default function App() {
const [activeNums, setActiveNums] = React.useState([]);
let nums = [];
for (let i = 1; i < 91; i++) {
nums.push(i);
}
const onToggle = (num) => {
setActiveNums((prevState) => {
if (prevState.includes(num)) {
return [...prevState.filter((n) => n !== num)];
} else {
return [...prevState, num];
}
});
};
return (
<div>
{nums.map((num) => {
return (
<button
key={num}
onClick={(e) => onToggle(num)}
className={activeNums.includes(num) ? "game_clicked" : ""}
>
{num}
</button>
);
})}
</div>
);
}
I'm trying to use react forwardRef to call a function inside bunch of child components. Here is the code.
const WorkoutFeedbackForm = ({
latestGameplaySession,
activityFeedbacks,
selectedActivityIndex,
setIsReady,
}) => {
const [isLoading, setIsLoading] = useState(false);
const workoutRef = createRef();
const refMap = new Map();
const onSubmitFeedbackClick = useCallback(async () => {
setIsLoading(true);
await workoutRef.current.onSubmitFeedback();
for (let i = 0; i < activityFeedbacks.length; i++) {
const activityRef = refMap.get(activityFeedbacks[i].sessionID);
console.log(activityRef);
if (activityRef && activityRef.current) {
activityRef.current.onSubmitFeedback();
}
}
setIsLoading(false);
}, [
activityFeedbacks,
refMap,
]);
return (
<>
<FeedbackFormContainer
key={`${latestGameplaySession.id}-form`}
name="Workout Feedback"
feedback={latestGameplaySession.coachFeedback}
isSelected
gameplaySessionDoc={latestGameplaySession}
pathArr={[]}
ref={workoutRef}
/>
{activityFeedbacks.map((feedback, index) => {
const activityRef = createRef();
refMap.set(feedback.sessionID, activityRef);
return (
<FeedbackFormContainer
key={feedback.sessionID}
name={feedback.name}
feedback={feedback.coachFeedback}
isSelected={index === selectedActivityIndex}
gameplaySessionDoc={latestGameplaySession}
pathArr={feedback.pathArr}
setIsReady={setIsReady}
ref={activityRef}
/>
);
})}
<FeedbackSubmit
onClick={onSubmitFeedbackClick}
isLoading={isLoading}
>
Save Feedbacks
</FeedbackSubmit>
</>
);
};
The problem is it seems createRef only works for the component outside the loop. Do you have any idea what's wrong here. Or is it not possible to do that?
I am trying to find out if a div has overflown text and show show more link if it does. I found this stackoverflow answer to check if a div is overflowing. According to this answer, I need to implement a function which can access styles of the element in question and do some checks to see if it is overflowing. How can I access the styles of an element. I tried 2 ways
1. Using ref
import React from "react";
import "./styles.css";
export default function App(props) {
const [showMore, setShowMore] = React.useState(false);
const onClick = () => {
setShowMore(!showMore);
};
const checkOverflow = () => {
const el = ref.current;
const curOverflow = el.style.overflow;
if ( !curOverflow || curOverflow === "visible" )
el.style.overflow = "hidden";
const isOverflowing = el.clientWidth < el.scrollWidth
|| el.clientHeight < el.scrollHeight;
el.style.overflow = curOverflow;
return isOverflowing;
};
const ref = React.createRef();
return (
<>
<div ref={ref} className={showMore ? "container-nowrap" : "container"}>
{props.text}
</div>
{(checkOverflow()) && <span className="link" onClick={onClick}>
{showMore ? "show less" : "show more"}
</span>}
</>
)
}
2. Using forward ref
Child component
export const App = React.forwardRef((props, ref) => {
const [showMore, setShowMore] = React.useState(false);
const onClick = () => {
setShowMore(!showMore);
};
const checkOverflow = () => {
const el = ref.current;
const curOverflow = el.style.overflow;
if (!curOverflow || curOverflow === "visible") el.style.overflow = "hidden";
const isOverflowing =
el.clientWidth < el.scrollWidth || el.clientHeight < el.scrollHeight;
el.style.overflow = curOverflow;
return isOverflowing;
};
return (
<>
<div ref={ref} className={showMore ? "container-nowrap" : "container"}>
{props.text}
</div>
{checkOverflow() && (
<span className="link" onClick={onClick}>
{showMore ? "show less" : "show more"}
</span>
)}
</>
);
});
Parent component
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
const rootElement = document.getElementById("root");
const ref = React.createRef();
ReactDOM.render(
<React.StrictMode>
<App
ref={ref}
text="Start editing to see some magic happen! Click show more to expand and show less to collapse the text"
/>
</React.StrictMode>,
rootElement
);
But I got the following error in both approaches - Cannot read property 'style' of null.
What am I doing wrong? How can I achieve what I want?
As Jamie Dixon suggested in the comment, I used useLayoutEffect hook to set showLink true. Here is the code
Component
import React from "react";
import "./styles.css";
export default function App(props) {
const ref = React.createRef();
const [showMore, setShowMore] = React.useState(false);
const [showLink, setShowLink] = React.useState(false);
React.useLayoutEffect(() => {
if (ref.current.clientWidth < ref.current.scrollWidth) {
setShowLink(true);
}
}, [ref]);
const onClickMore = () => {
setShowMore(!showMore);
};
return (
<div>
<div ref={ref} className={showMore ? "" : "container"}>
{props.text}
</div>
{showLink && (
<span className="link more" onClick={onClickMore}>
{showMore ? "show less" : "show more"}
</span>
)}
</div>
);
}
CSS
.container {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 200px;
}
.link {
text-decoration: underline;
cursor: pointer;
color: #0d6aa8;
}
We could create a custom hooks to know if we have overflow.
import * as React from 'react';
const useIsOverflow = (ref, isVerticalOverflow, callback) => {
const [isOverflow, setIsOverflow] = React.useState(undefined);
React.useLayoutEffect(() => {
const { current } = ref;
const { clientWidth, scrollWidth, clientHeight, scrollHeight } = current;
const trigger = () => {
const hasOverflow = isVerticalOverflow ? scrollHeight > clientHeight : scrollWidth > clientWidth;
setIsOverflow(hasOverflow);
if (callback) callback(hasOverflow);
};
if (current) {
trigger();
}
}, [callback, ref, isVerticalOverflow]);
return isOverflow;
};
export default useIsOverflow;
and just check in your component
import * as React from 'react';
import { useIsOverflow } from './useIsOverflow';
const App = () => {
const ref = React.useRef();
const isOverflow = useIsOverflow(ref);
console.log(isOverflow);
// true
return (
<div style={{ overflow: 'auto', height: '100px' }} ref={ref}>
<div style={{ height: '200px' }}>Hello React</div>
</div>
);
};
Thanks to Robin Wieruch for his awesome articles
https://www.robinwieruch.de/react-custom-hook-check-if-overflow/
Solution using TS and Hooks
Create your custom hook:
import React from 'react'
interface OverflowY {
ref: React.RefObject<HTMLDivElement>
isOverflowY: boolean
}
export const useOverflowY = (
callback?: (hasOverflow: boolean) => void
): OverflowY => {
const [isOverflowY, setIsOverflowY] = React.useState(false)
const ref = React.useRef<HTMLDivElement>(null)
React.useLayoutEffect(() => {
const { current } = ref
if (current) {
const hasOverflowY = current.scrollHeight > window.innerHeight
// RHS of assignment could be current.scrollHeight > current.clientWidth
setIsOverflowY(hasOverflowY)
callback?.(hasOverflowY)
}
}, [callback, ref])
return { ref, isOverflowY }
}
use your hook:
const { ref, isOverflowY } = useOverflowY()
//...
<Box ref={ref}>
...code
Import your files as need be and update code to your needs.