Basically I was trying to render a really really long list (potentially async) in React and I only want to render the visible entries±10 up and down.
I decided to get the height of the component that's holding the list, then calculate the overall list height/row height, as well as the scroll position to decide where the user have scrolled.
In the case below, SubWindow is a general component that could hold a list, or a picture, etc... Therefore, I decided it wasn't the best place for the calculations. Instead, I moved the calc to a different component and tried to use a ref instead
const BananaWindow = (props) => {
const contentRef = useRef(null)
const [contentRefHeight, setContentRefHeight] = useState(0)
useEffect(()=>setContentRefHeight(contentRef.current.offsetHeight), [contentRef])
//calc which entries to include
startIdx = ...
endIdx = ...
......
return (
<SubWindow
ref={contentRef}
title="all bananas"
content={
<AllBananas
data={props.data}
startIdx={startIdx}
endIdx={endIdx}
/>
}
/>
)
}
//this is a more general component. accepts a title and a content
const SubWindow = forwardRef((props, contentRef) => {
return (
<div className="listContainer">
<div className="title">
{props.title}
</div>
<div className="list" ref={contentRef}>
{props.content}
</div>
</div>
})
//content for all the bananas
const AllBanana = (props) => {
const [data, setData] = useState(null)
//data could be from props.data, but also could be a get request
if (props.data === null){
//DATA FETCHING
setData(fetch(props.addr).then()...)
}
return(
<Suspense fallback={<div>loading...</div>}>
//content
</Suspense>
}
PROBLEM: In BananaWindow, the useEffect is triggered only for initial mounting and painting. So I only ended up getting the offsetWidth of the placeholder. The useEffect does nothing when the content of SubWindow finishes loading.
UPDATE: Tried to use callback ref and it still only showed the height of the placeholder. Trying resize observer. But really hope there's a simpler/out of the box way for this...
So I solved it using ResizeObserver. I modified the hook from this repo to fit my project.
Related
Good day!
I would like to ask if someone know how to render the HTML dynamically after passing a props from my customized component. since I only noticed that initialContentHTML props can render HTML when the component is on mount stage.
Thank you!
// My Component
const CheckboxDetails = (props) => {
const {
currentIndex = 0,
onChangeRichText,
richTextRef,
initialContentHTML,
disabled,
} = props;
// Rich Text Component from "react-native-pell-rich-editor"
<RichEditor
ref={(el) => { return richTextRef.current[currentIndex] = el; }}
useContainer={false}
containerStyle={{ minHeight: height }}
onChange={onChangeRichText}
editorInitializedCallback={editorInitializedCallback}
initialContentHTML={initialContentHTML}
disabled={disabled}
/>
}
export default CheckboxDetails
I was planning to change another module, however it will take longer to modify our system and might as well affect some part of our system.
Using ref you can call html modification method setContentHTML or insertHTML
const editor = useRef(null);
const changeHTML = () => editor.current?.setContentHTML('some HTML here')
<RichEditor
ref={editor}
initialContentHTML={'Hello <b>World</b> <p>this is a new paragraph</p> <p>this is another new paragraph</p>'}
/>
I was thinking about how to code TailwindCSS cleaner in React. Since Tailwind is utility-first, it makes us inevitably end up with components (ex: className="w-full bg-red-500"). So, I tried to create a utility like this:
utils/tailwind.ts
const tw = (...classes: string[]) => classes.join(' ')
and call it inside:
components/Example.tsx
import { useState } from 'react'
import tw from '../utils/tailwind'
const Example = () => {
const [text, setText] = useState('')
return (
<div>
<input onChange={(e: any) => setText(e.target.value)} />
<div
className={tw(
'w-full',
'h-full',
'bg-red-500'
)}
>
hello
</div>
</div>
)
}
But, it will cause tw() to be re-called as always as text state is updated.
So, I decided to wrap tw() function using useMemo to prevent re-call since the tw() always returns the same value. But the code is like this:
import { useState, useMemo } from 'react'
import tw from '../utils/tailwind'
const Example = () => {
const [text, setText] = useState('')
return (
<div>
<input onChange={(e: any) => setText(e.target.value)} />
<div
className={useMemo(() => tw(
'w-full',
'h-full',
'bg-red-500'
), [])}
>
hello
</div>
</div>
)
}
Is it correct or good practice if I put useMemo like that? Thank you 🙏 .
Is it correct or good practice if I put useMemo like that?
Short answer - yes.
Long answer - it depends. It depends on how heavy the operation is. In your particular case, joining a couple of strings may not be such heavy calculation to make the useMemo worth to be used - it's good to remember that useMemo memoizes stuff and it takes memory.
Consider example below. In the first case, without useMemo, the tw function will be called with every App re-render, to calculate new className. However, if useMemo is used (with empty dependency array), tw will not be called and new className will not be calculated even if the App re-renders, due to the basic memoization. It will be called only once, on component mount.
Conclusion - it's a good practice to use useMemo, but rather for heavy operations, like mapping or reducing huge arrays.
export default function App() {
const [_, s] = useState(0);
return (
<div className="App">
<div className={tw(false, 'w-full', 'h-full', 'bg-red-500')}>div1</div>
<div
className={useMemo(
() => tw(true, 'w-full', 'h-full', 'bg-red-500'),
[],
)}
>
div2
</div>
<button onClick={() => s(Math.random())}>re-render</button>
</div>
);
}
Playground: https://codesandbox.io/s/distracted-liskov-tfm72c?file=/src/App.tsx
The issue here is that React will re-render the component every time it's state changes. (each time you setText).
If you want to prevent that from happening, then see if you really need this re-render hence what do you really need the input text for?
you do not HAVE to use state here to use the input value.
you could call another function on change which will not update the state, and use the input value there for whatever you need.
for example:
const Example = () => {
const onInputChange = (e) => {
const text = e.target.value
// do something with text
}
return (
<div>
<input onChange={(e: any) => onInputChange(e)} />
<div
className={useMemo(() => tw(
'w-full',
'h-full',
'bg-red-500'
), [])}
>
hello
</div>
</div>
)
}
There are some movie cards that clients can click on them and their color changes to gray with a blur effect, meaning that the movie is selected.
At the same time, the movie id is transferred to an array list. In the search bar, you can search for your favorite movie but the thing is after you type something in the input area the movie cards that were gray loses their style (I suppose because they are deleted and rendered again based on my code) but the array part works well and they are still in the array list.
How can I preserve their style?
Search Page:
export default function Index(data) {
const info = data.data.body.result;
const [selectedList, setSelectedList] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
return (
<>
<main className={parentstyle.main_container}>
<NavBar />
<div className={style.searchbar_container}>
<CustomSearch
onChange={(e) => {
setSearchTerm(e.target.value);
}}
/>
</div>
<div className={style.card_container}>
{info
.filter((value) => {
if (searchTerm === '') {
return value;
} else if (
value.name
.toLocaleLowerCase()
.includes(searchTerm.toLocaleLowerCase())
) {
return value;
}
})
.map((value, key) => {
return (
<MovieCard
movieName={value.name}
key={key}
movieId={value._id}
selected={selectedList}
setSelected={setSelectedList}
isSelected={false}
/>
);
})}
</div>
<div>
<h3 className={style.test}>{selectedList}</h3>
</div>
</main>
Movie Cards Component:
export default function Index({ selected, movieName, movieId, setSelected }) {
const [isActive, setActive] = useState(false);
const toggleClass = () => {
setActive(!isActive);
};
useEffect(()=>{
})
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
selected.splice(selected.indexOf(e.target.id), 1);
console.log(selected);
} else {
selected.push(e.target.id);
console.log(selected);
console.log(e.target);
}
setSelected([...selected]);
toggleClass();
};
return (
<div>
<img
className={isActive ? style.movie_selected : style.movie}
id={movieId}
name={movieName}
src={`images/movies/${movieName}.jpg`}
alt={movieName}
onClick={pushToSelected}
/>
<h3 className={style.title}>{movieName}</h3>
</div>
);
}
I can't directly test your code so I will assume that this is the issue:
Don't directly transform a state (splice/push) - always create a clone or something.
Make the setActive based on the list and not dependent. (this is the real issue why the style gets removed)
try this:
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
// filter out the id
setSelected(selected.filter(s => s !== e.target.id));
return;
}
// add the id
setSelected([...selected, e.target.id]);
};
// you may use useMemo here. up to you.
const isActive = selected.includes(movieId);
return (
<div>
<img
className={isActive ? style.movie_selected : style.movie}
id={movieId}
name={movieName}
src={`images/movies/${movieName}.jpg`}
alt={movieName}
onClick={pushToSelected}
/>
<h3 className={style.title}>{movieName}</h3>
</div>
);
This is a very broad topic. The best thing you can do is look up "React state management".
As with everything in the react ecosystem it can be handled by various different libraries.
But as of the latest versions of React, you can first start by checking out the built-in tools:
Check out the state lifecycle: https://reactjs.org/docs/state-and-lifecycle.html
(I see in your example you are using useState hooks, but I am adding these for more structured explanation for whoever needs it)
Then you might want to look at state-related hooks such as useState: https://reactjs.org/docs/hooks-state.html
useEffect (to go with useState):
https://reactjs.org/docs/hooks-effect.html
And useContext:
https://reactjs.org/docs/hooks-reference.html#usecontext
And for things outside of the built-in toolset, there are many popular state management libraries that also work with React with the most popular being: Redux, React-query, Mobx, Recoil, Flux, Hook-state. Please keep in mind that what you should use is dependant on your use case and needs. These can also help you out to persist your state not only between re-renders but also between refreshes of your app. More and more libraries pop up every day.
This is an ok article with a bit more info:
https://dev.to/workshub/state-management-battle-in-react-2021-hooks-redux-and-recoil-2am0#:~:text=State%20management%20is%20simply%20a,you%20can%20read%20and%20write.&text=When%20a%20user%20performs%20an,occur%20in%20the%20component's%20state.
I am attempting to create a Panel component that has the option to be wrapped in a PanelGroup component. The PanelGroup components main function is the query the children Panel components and determine if certain conditions so it can add a z-index prop to each of them according to the conditions.
Is it a left, right, top or bottom positioned Panel
Is it a nested Panel
Each condition will require a z-index that may be higher than the others and also placed in front of the highest z-index found on the page. I would like to be able to iterate though the children inside a useEffect and after logic is ran to determine the hierarchy, add the z-index to each child as a prop. I have attempted to use React.Children mapping through and using cloneElement to add the z-index, however the issue is this method must be called inside the render/return and when a second panel is opened it causes any other panel that is open to close and open again. Is there a way to update the props without using the top level React API?
PanelGroup API
<PanelGroup>
<Panel
id={'1'}
open={open1}
onClosed={handleClose1}
controller={true}
renderPortal={true}
position={PanelPosition.RIGHT}
>
<div>
<h2>Panel Slide Left</h2>
<button onClick={handleClose1}>Close</button>
</div>
</Panel>
<Panel
id={'2'}
open={open2}
onClosed={handleClose2}
controller={true}
renderPortal={true}
position={PanelPosition.LEFT}
>
<div>
<h2>Panel Slide Right</h2>
<button onClick={handleClose2}>Close</button>
</div>
</Panel>
</PanelGroup>
This is what I tried which does not work
const PanelGroup = ({
children
}: PanelGroupProp): JSX.Element => {
const [highestZIndex, setHighestZIndex] = React.useState(null);
/**
* Handles finding the highest index on the page after render. This value
* will be used as the benchmark to set the z-index of the panel
* components managed by this component.
*/
React.useEffect(() => {
const highestIndex = getHighestZIndex();
setHighestZIndex(highestIndex);
}, []);
const renderGroupPanelChildren = (children): React.ReactElement => {
let incrementor = highestZIndex + 1;
return (
<React.Fragment>
{React.Children.map(children || null, (child) => {
const newProps = {...child.props, ZIndex: incrementor };
incrementor++;
return <child.type {...newProps } key={getGuid()} />;
})}
</React.Fragment>
);
};
return (
<>
<PanelOverlay visibility={false} />
{renderGroupPanelChildren(children)}
</>
);
};
I would like to find a way to do something like:
const PanelGroupComponent = ({
children
}: PanelGroupProp): JSX.Element => {
const [highestZIndex, setHighestZIndex] = React.useState(null);
const [elements, setElements] = React.useState(null);
/**
* Handles finding the highest index on the page after render. This value
* will be used as the benchmark to set the z-index of the panel
* components managed by this component.
*/
React.useEffect(() => {
const highestIndex = getHighestZIndex();
setHighestZIndex(highestIndex);
}, []);
React.useEffect(() => {
// logic to set z-index's ....
const elements ....
setElements(elements);
}, [highestIndex]);
return (
<>
<PanelOverlay visibility={false} />
{elements}
</>
);
};
Basically i'm looking for react table library that can take a mutable object ( to be specific an useRef object) as the main source of data to be displayed.
Basically i want to do something like this:
const TableViewComponent = () =>{
const tableData = useRef([{ rowName: 'test', value:0}] -> basically an array of objects that represents data for every row (the structure doesnt matter)
# code that receives data from server and updates the tableData.current with the data needed
return(
<Table data={tableData.current}/>
)
}
Basically, since i get a bunch of messages from the server and i update the data constantly (the number of rows stays the same), i don't want to rerender the table everytime. So i want to use the useRef to change the data thats being displayed on the table without triggering a rerender from react.
Im not sure if its something that can be done but any help is appreciated :). I tried react-table, rc-table but they didnt seem to work.
Basically, it looks to me like you'll have to do it yourself.
There's some libraries that might help you (like useTable which focuses on headless components) but I don't think they offer what you're looking for.
So let's quickly do a mock-up! (note: this is a quick sketch, assume that the undefined variables are stored somewhere else or are given from the fetch)
function useTableData({ initialData, itemsPerPage, ...etc }) {
const data = useRef(initialData);
const [loading, setLoading] = useState(false);
useEffect(() => {
data.current = fetchFromSomeWhere(...etc);
() => (data.current = null);
}, [etc /* Place other dependencies that invalidate out data here*/]);
const handleNewPage = useCallback(
async ({ pageIndex }) => {
if (!data.current[pageIndex * itemsPerPage]) {
setLoading(true);
data.current = [...data.current, await fetchMoreData(pageIndex)];
}
setLoading(false);
return data.current;
},
[itemsPerPage, data, setLoading],
);
return [data, handleNewPage, loading];
}
Notice how every single thing returned from this hook is a constant reference except for the loading! Meaning, that we can safely pass this to a memoized table, and it won't trigger any re-renders.
const TableContainer = memo(etc => {
const [data, handleNewPage, loading] = useDataForTable(...etc);
return (
<>
{loading && <Spinner />}
{/* Look ma, no expensive-renders! */}
<Body {...{ data }} />
<Pagination {...{ handleNewPage }} />
<OtherActions />
</>
);
});
However, you might have noticed that the body won't re-render when we click on the next page! Which was the whole point of pagination! So now we need some sort of state that'll force the Body to re-render
const TableContainer = memo(etc => {
const [currentPage, setPage] = useState(0);
const [data, handleNewPage, loading] = useDataForTable(...etc);
return (
<>
{loading && <Spinner />}
{/* We're still re-rendering here :( */}
<Body {...{ data, currentPage }} />
<Footer {...{ handleNewPage, setPage }} />
<OtherActions />
</>
);
});
And so we're left with a table that to use properly we need to:
Exercise restraint and properly invalidate old data. Otherwise, we'd be displaying incorrect data.
'Unpack' the data from the current ref on the Body component, and then render it.
In essence, after all that work we're still left with a solution isn't particularly attractive, unless you have some really complicated actions or some expensive scaffolding around the TableComponent itself. This might however be your case.