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.
Related
I have been asked to solve a problem using any front-end framework ( reactjs in my case ) . Basically there is a form for adding a car and there can be say a type of car like 'SUV','semitruck','racecar' and so on...
In the form there needs to be different components rendered based on the type of car you want to add ( e.g. SUV has 2 inputs , racecar has 5 inputs ) how can i dynamically render these components and get their input values without doing if statements?
So pretty much i want to avoid doing this :
{typeSelection == "SUV" && (
<SVUInput
size={sizeInput}
changeSize={(str) => setSizeInput(str)}
/>
)}
{typeSelection == "Bus" && (
<WeightInput
Weight={weightInput}
changeWeight={(str) => setWeightInput(str)}
/>
)}
{typeSelection == "Semitruck" && (
<DimensionInput
wheels={wheels}
length={length}
changeWheels={(n) => setWheels(n)}
changeLength={(str) => setLength(str)}
/>
)}
I tried doing this but doesnt work ( im guessing react doesnt re-render here )
const [dynamicInputs, setDynamicInputs] = React.useState<any>({
SUV: <SUVInput size={sizeInput} changeSize={(str) => setSizeInput(str)} />,
bus: <BusInput weight={weightInput} changeSize={(n) => setSizeInput(n)} />,
semitruck: <SemitruckInput wheels={wheelsInput} changeWheels={(n) => setWheelsInput(n)}
color={colorInput} changeColor={(n) => setColorInput(n)} />,
});
Instead the above code although renders the component , the input does not change when i type into it, it remains blank , i assume it doesnt trigger react to re-render the state
So pretty much instead of making many if statements that would slow down the front-end , Im trying to make it dynamic so that it only takes O(1) time to render the correct form inputs.
I pretty like your solution because that's the thing I'm used to do really often. Your issue is about storing the components inside useState, because they are initialized there when component mounts and they just stay in the same state for the whole component lifetime. The solution is pretty simple as well - just move it out of the state so they do react to state and props changes.
https://codesandbox.io/s/frosty-wozniak-xcy2bv?file=/src/App.js:114-536
export default function App() {
const [size, setSize] = useState(0);
const [currentComponent] = useState('SUV');
const components = {
SUV: <SuvComponent size={size} />,
};
return (
<div className="App">
<input onChange={(e) => setSize(e.target.value)} />
{components[currentComponent]}
</div>
);
}
it works if i implement the UseState method and then have a useEffect to update it like so :
React.useEffect(() => {
setDynamicInputs({
SUV: (
<SUV size={sizeInput} changeSize={(str) => setSizeInput(str)} />
),
bus: (
<WeightInput
Weight={weightInput}
changeWeight={(str) => setWeightInput(str)}
/>
),
});
}, [dynamicInputs]);
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.
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.
I have a similar situation like the one in the sandbox.
https://codesandbox.io/s/react-typescript-fs0em
Basically what I want to achieve is that Table.tsx is my base component and App component is acting like a wrapper component. I am returning the JSX from the wrapper file.
Everything is fine but the problem is whenever I hover over any name, getData() is called and that is too much rerendering. Here it is a simple example but in my case, in real, the records are more.
Basically Table is a generic component which can be used by any other component and the data to be displayed in can vary. For e.g. rn App is returning name and image. Some other component can use the Table.tsx component to display name, email, and address. Think of App component as a wrapper.
How can I avoid this getData() to not to be called again and again on hover?
Can I use useMemo or what approach should I use to avoid this?
Please help
Every time you update the "hover" index state in Table.jsx it rerenders, i.e. the entire table it mapped again. This also is regenerating the table row JSX each time, thus why you see the log "getData called!" so much.
You've effectively created your own list renderer, and getData is your "renderRow" function. IMO Table shouldn't have any state and the component being rendered via getData should handle its own hover state.
Create some "row component", i.e. the thing you want to render per element in the data array, that handles it's own hover state, so when updating state it only rerenders itself.
const RowComponent = ({ index, name }) => {
const [hov, setHov] = useState();
return (
<div
key={name}
onMouseEnter={() => setHov(index)}
onMouseLeave={() => setHov(undefined)}
style={{ display: "flex", justifyContent: "space-around" }}
>
<div> {name} </div>
<div>
<img
src={hov === index ? img2 : img1}
height="30px"
width="30px"
alt=""
/>
</div>
</div>
);
};
Table.jsx should now only take a data prop and a callback function to render a specific element, getData.
interface Props {
data: string[];
getData: () => JSX.Element;
}
export const Table: React.FC<Props> = ({ data, getData }) => {
return (
<div>
{data.map((name: string, index: number) => getData(name, index))}
</div>
);
};
App
function App() {
const data = ["Pete", "Peter", "John", "Micheal", "Moss", "Abi"];
const getData = (name: string, index: number, hov: number) => {
console.log("getData called!", index);
return <RowComponent name={name} index={index} />;
};
return <Table data={data} getData={getData} />;
}
I am new to React Hooks. I write a small code that displays a list of courses to the user. It contains 2 main components CourseList and Course and a custom hook useCourseList. Here is the code of the custom hook:
function useCourseList(searchString) {
const [courses, setCourses] = useState([]);
useEffect(() => {
function handleCourseListUpdating(nextCourses) {
setCourses(nextCourses);
}
getCourses(searchString, handleCourseListUpdating)
return () => {
setCourses([]);
}
}, [searchString]);
return courses;
}
The above code is fetching api data by a search string via getCourses function. Then it updates the retrieved data to courses state which is rendered by CourseList component. The useEffect function is triggered with searchString as its dependencies to keep updating courses' data once the search string changes.
The web browser refreshes many times corresponding with every single character change of the search string although the data results on those keyword changes are the same.
For example, we are looking for a game course with keyword "Game". Then it calls 4 times on useEffect one by one with "G", "Ga", "Gam", and "Game". Those keywords give the same results with my current api data but they are constantly updated React DOM via web interface. That leads to browser twinkling 4 times which is unnecessary. It also gives a bad user experience.
Is there any solution to prevent updating UI as data are still consistent? Is there any problem with the data flow?
For demonstration, you can see the full sample code here:
CodeSandbox
There are 2 changes you can make for a better ui experience
Debounce the API call for search
Do not set the state to empty in useEffect cleanup
You can choose to write your own custom debounce function or use it from a library such as lodash or underscore. Once you do that you can use useMemo to create a debounced instance of the getCourses function and call it inside useEffect
const debounce = (fn, delay) => {
let timer = null;
return function(...args) {
if (timer){
clearInterval(timer);
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay)
}
}
// A custom Hook
function useCourseList(searchString) {
const [courses, setCourses] = useState([]);
const getDebouncedCourses = useMemo(() => debounce(getCourses, 300), []);
useEffect(() => {
function handleCourseListUpdating(courses) {
setCourses(courses);
}
getDebouncedCourses(searchString, handleCourseListUpdating);
}, [searchString, getDebouncedCourses]);
return courses;
}
// CourseList component
function CourseList() {
const [searchString, setSearchString] = useState('');
const courses = useCourseList(searchString)
return (
<div>
{courses ? (
<div>
<TextField style={{ padding: 24 }}
id="searchInput"
placeholder="Search for Courses"
margin="normal"
onChange={event => setSearchString(event.target.value)}
/>
<Grid container spacing={4} style={{ padding: 24 }}>
{courses.map((course, index) => (
<Grid key={index} item xs={12} sm={6} lg={4} xl={3}>
<Course course={course} />
</Grid>
))}
</Grid>
</div>
) : "No courses found"}
</div>
);
}