How to handle expensive functions in React - reactjs

I have a component in my application that handles two somewhat expensive functions, but not always at the same time. In one application I have a globe that uses window.requestAnimationFrame() to update the rotation on every request and spin. This runs smoothly when I'm interacting with the other parts of component.
The problem arises when the user interacts with a search, that filters an array of objects.
When I'm typing the globe spin animation runs smoothly, once the getItems() function is called it stutters/stops until the getItems() function is done.
The Map component and the Search component are seperate but are both rendered by a higher component. When the user is typing in the search, there is no reason for the higher component to rerender, and thus the map component does not rerender. This suggests that the problem is the expensive function that hinders the application as a whole.
The item list is defined when the component mounts in an UseEffect hook. The json file that holds the objects lives in the public folder, and is about 30MB. When the users searches, it runs the getItems() functions. I already have a setTimeout on the inputValueChange so it only runs once the user is presumable done typing. Here is the code in question.
import { useEffect, useState, useRef, useCallback, CSSProperties } from 'react'
import { useVirtual } from 'react-virtual'
import { useCombobox, UseComboboxStateChange } from 'downshift'
import { v4 as uuid } from 'uuid';
import { Hint } from 'react-autocomplete-hint';
import MarkerObject from '../../../interfaces/MarkerObjectInterface';
interface Props {
items: Array<MarkerObject>;
onSelect(item: MarkerObject): void;
onHoverEnter: (item: MarkerObject) => void;
onHoverLeave: () => void;
}
const Search = (props: Props) => {
//Initialize the state and refs
const [inputValue, setInputValue] = useState('')
const [items, setItems] = useState<MarkerObject[]>([])
const listRef = useRef<HTMLElement>(null)
//Waits for the user to stop typing for half a second before updating the list of items
//This helps a lot with an unnecescary rerender of the somewhat expensive getItems function
useEffect(() => {
const delayInput = setTimeout(() => {
setItems(getItems(inputValue))
}, 500);
return () => clearTimeout(delayInput);
}, [inputValue])
//Listens for a selected item, then passes that item back to TestMaped Item: ", selectedItem)
const handleSelectedItemChange = (changes: UseComboboxStateChange<any>) => {
props.onSelect(changes.selectedItem)
// props.onHoverLeave(changes.selectItem)
}
//Filters the items based on seach query
function getItems(search: string) {
if (search.length === 0)
return []
return props.items.filter((n) => {
if (n.type === "city") {
const citystatecountry = (n.name + ", " + n.state_name + ", " + n.country_name).toLowerCase()
const citystatecodecountry = (n.name + ", " + n.state_code + ", " + n.country_name).toLowerCase()
const citystatecodecountrycode = (n.name + ", " + n.state_code + ", " + n.country_code).toLowerCase()
const citystate = (n.name + ", " + n.state_name).toLowerCase()
const citystatecode = (n.name + ", " + n.state_code).toLowerCase()
const citycountry = (n.name + ", " + n.country_name).toLowerCase()
const citycountrycode = (n.name + ", " + n.country_code).toLowerCase()
const city = n.name.toLowerCase()
if (citystatecountry.startsWith(search.toLowerCase()))
return true
else if (citystate.startsWith(search.toLowerCase()))
return true
else if (citycountry.startsWith(search.toLowerCase()))
return true
else if (city.startsWith(search.toLowerCase()))
return true
else if (citystatecodecountry.startsWith(search.toLowerCase()))
return true
else if (citystatecodecountrycode.startsWith(search.toLowerCase()))
return true
else if (citystatecode.startsWith(search.toLowerCase()))
return true
else if (citycountrycode.startsWith(search.toLowerCase()))
return true
}
else if (n.type === "state") {
const state = (n.name).toLowerCase()
const statecountry = (n.name + ", " + n.country_name).toLowerCase()
if (state.startsWith(search.toLowerCase()))
return true
else if (statecountry.startsWith(search.toLowerCase()))
return true
}
else {
const country = (n.name).toLowerCase()
if (country.startsWith(search.toLowerCase()))
return true
}
return false
})
}
//Initialies the itemToString funciton to be passed to useCombobox
const itemToString = (item: MarkerObject | null) => (item ? item.display_text : '')
//Initializes the rowVirtualizer using a fixed size function
const rowVirtualizer = useVirtual({
size: items.length,
parentRef: listRef,
estimateSize: useCallback(() => 30, []),
overscan: 1,
})
const {
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
highlightedIndex,
selectedItem,
getComboboxProps,
isOpen,
selectItem
} = useCombobox({
items,
itemToString,
inputValue,
onSelectedItemChange: (changes) => handleSelectedItemChange(changes),
onInputValueChange: (d) => {
console.log(d.inputValue)
if (d.inputValue || d.inputValue === "")
setInputValue(d.inputValue)
},
scrollIntoView: () => { },
onHighlightedIndexChange: ({ highlightedIndex }) => {
if (highlightedIndex)
rowVirtualizer.scrollToIndex(highlightedIndex)
},
})
return (
<div className="" onMouseLeave={() => props.onHoverLeave()}>
<div className="">
<label {...getLabelProps()}>Choose an element:</label>
<div {...getComboboxProps()}>
<input
className={inputCss}
{...getInputProps(
{
type: 'text',
placeholder: 'Enter Place',
})}
/>
</div>
</div>
<ul className="-webkit-scrollbar-track:purple-500"
{...getMenuProps({
ref: listRef,
style: menuStyles,
})}
>
{isOpen && (
<>
<li key="total-size" style={{ height: rowVirtualizer.totalSize }} />
{rowVirtualizer.virtualItems.map((virtualRow) => (
<li
key={uuid()}
{...getItemProps({
index: virtualRow.index,
item: items[virtualRow.index],
style: {
backgroundColor:
highlightedIndex === virtualRow.index
? 'lightgray'
: 'inherit',
fontWeight:
selectedItem &&
selectedItem.id === items[virtualRow.index].id
? 'bold'
: 'normal',
position: 'absolute',
fontSize: '1.2em',
top: 0,
left: 0,
width: '100%',
height: virtualRow.size,
transform: `translateY(${virtualRow.start}px)`,
},
})}
onMouseEnter={() => props.onHoverEnter(items[virtualRow.index])}
onMouseLeave={() => props.onHoverLeave()}
onMouseOut={() => props.onHoverLeave()}
>
{items[virtualRow.index].display_text}
</li>
))}
</>
)}
</ul>
</div>
)
}
export default Search
I'm also using react-virtual with downshift (example https://www.downshift-js.com/use-combobox#virtualizing-items-with-react-virtual). This definitely improves performance when interacting with the dropdown box as there is virtually no stutter on the animation.
I'm curious about what my options are here. I'm also using firestore with my application. Is there way to allocate memory or limit the memory used by this function or component? Is there a better algorithm or function I should be using in place of .filter()? Should I even have that large of a file in the public folder?
Or should I put the whole json object in a firestore collection? Because its 30MBs and I would want to query across the document, I would have to separate the data into documents. This seems like it would add a huge amount of document reads. Any help or suggestions would be greatly appreciated!

Related

React | Collect State Values of Children Array and update Per Object and Save to PouchDB

Stackoverflow
problem
I have separate components that house Tiptap Editor tables. At first I had a save button for each Child Component which worked fine, but was not user friendly. I want to have a unified save button that will iterate through each child Table component and funnel all their editor.getJSON() data into an array of sections for the single doc object . Then finish it off by saving the whole object to PouchDB
What did I try?
link to the repo → wchorski/Next-Planner: a CRM for planning events built on NextJS (github.com)
Try #1
I tried to use the useRef hook and the useImperativeHandle to call and return the editor.getJSON(). But working with an Array Ref went over my head. I'll post some code of what I was going for
// Parent.jsx
const childrenRef = useRef([]);
childrenRef.current = []
const handleRef = (el) => {
if(el && !childrenRef.current.includes(el)){
childrenRef.current.push(el)
}
}
useEffect(() =>{
childrenRef.current[0].childFunction1() // I know this doesn't work, because this is where I gave up
})
// Child.jsx
useImperativeHandle(ref, () => ({
childFunction1() {
console.log('child function 1 called');
},
childFunction2() {
console.log('child function 2 called');
},
}))
Try #2
I set a state counter and passed it down as a prop to the Child Component . Then I update the counter to trigger a child function
// Parent.jsx
export const Planner = ({id, doc, rev, getById, handleSave, db, alive, error}) => {
const [saveCount, setSaveCount] = useState(0)
const handleUpdate = () =>{
setSaveCount(prev => prev + 1)
}
const isSections = () => {
if(sectionsState[0]) handleSave(sectionsState)
if(sectionsState[0] === undefined) console.log('sec 0 is undefined', sectionsState)
}
function updateSections(newSec) {
setsectionsState(prev => {
const newState = sectionsState.map(obj => {
if(!obj) return
if (obj.header === newSec.header) {
return {...obj, ...newSec}
}
// 👇️ otherwise return object as is
return obj;
});
console.log('newState', newState);
return newState;
});
}
useEffect(() => {
setsectionsState(doc.sections)
}, [doc])
return (<>
<button
title='save'
className='save'
onPointerUp={handleUpdate}>
Save to State <FiSave />
</button>
<button
style={{right: "0", width: 'auto'}}
title='save'
className='save'
onClick={isSections}>
Save to DB <FiSave />
</button>
{doc.sections.map((sec, i) => {
if(!sec) return
return (
<TiptapTable
key={i}
id={id}
rev={doc.rev}
getById={getById}
updateSections={updateSections}
saveCount={saveCount}
section={sec}
db={db}
alive={alive}
error={error}
/>
)
})}
</>)
// Child.jsx
export const TiptapTable = ((props, ref) => {
const {id, section, updateSections, saveCount} = props
const [currTimeStart, setTimeStart] = useState()
const [defTemplate, setdefTemplate] = useState('<p>loading<p>')
const [isLoaded, setIsLoaded] = useState(false)
const [notesState, setnotesState] = useState('')
const editor = useEditor({
extensions: [
History,
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow.extend({
content: '(tableCell | tableHeader)*',
}),
TableHeader,
TableCell,
],
// i wish it was this easy
content: (section.data) ? section.data : defTemplate,
}, [])
const pickTemplate = async (name) => {
try{
const res = await fetch(`/templates/${name}.json`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json()
setIsLoaded(true)
setdefTemplate(data)
console.log('defTemplate, ', defTemplate);
// return data
} catch (err){
console.warn('template error: ', err);
}
}
function saveData(){
console.log(' **** SAVE MEEEE ', section.header);
try{
const newSection = {
header: section.header,
timeStart: currTimeStart,
notes: notesState,
data: editor.getJSON(),
}
updateSections(newSection)
} catch (err){
console.warn('table update error: ', id, err);
}
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) saveData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
useEffect(() => {
setTimeStart(section.timeStart)
setnotesState(section.notes)
if(!section.data) pickTemplate(section.header).catch(console.warn)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, section, isLoaded])
useEffect(() => {
if (editor && !editor.isDestroyed) {
if(section.data) editor.chain().focus().setContent(section.data).run()
if(!section.data) editor.chain().focus().setContent(defTemplate).run()
setIsLoaded(true)
}
}, [section, defTemplate, editor]);
if (!editor) {
return null
}
return isLoaded ? (<>
<StyledTableEditor>
<div className="title">
<input type="time" label='Start Time' className='time'
onChange={(e) => setTimeStart(e.target.value)}
defaultValue={currTimeStart}
/>
<h2>{section.header}</h2>
</div>
<EditorContent editor={editor} className="tiptap-table" ></EditorContent>
// ... non relavent editor controls
<button
title='save'
className='save2'
onPointerUp={() => saveData()}>
Save <FiSave />
</button>
</div>
</nav>
</StyledTableEditor>
</>)
: null
})
TiptapTable.displayName = 'MyTiptapTable';
What I Expected
What I expected was the parent state to update in place, but instead it overwrites the previous tables. Also, once it writes to PouchDB it doesn't write a single piece of new data, just resolved back to the previous, yet with an updated _rev revision number.
In theory I think i'd prefer the useRef hook with useImperativeHandle to pass up the data from child to parent.
It looks like this question is similar but doesn't programmatically comb through the children
I realize I could have asked a more refined question, but instead of starting a new question I'll just answer my own question from what I've learned.
The problem being
I wasn't utilizing React's setState hook as I iterated and updated the main Doc Object
Thanks to this article for helping me through this problem.
// Parent.jsx
import React, {useState} from 'react'
import { Child } from '../components/Child'
export const Parent = () => {
const masterDoc = {
_id: "123",
date: "2023-12-1",
sections: [
{header: 'green', status: 'old'},
{header: 'cyan', status: 'old'},
{header: 'purple', status: 'old'},
]
}
const [saveCount, setSaveCount] = useState(0)
const [sectionsState, setsectionsState] = useState(masterDoc.sections)
function updateSections(inputObj) {
setsectionsState(prev => {
const newState = prev.map(obj => {
// 👇️ if id equals 2, update country property
if (obj.header === inputObj.header)
return {...obj, ...inputObj}
return obj;
});
return newState;
});
}
return (<>
<h1>Parent</h1>
{sectionsState.map((sec, i) => {
if(!sec) return
return (
<Child
key={i}
section={sec}
updateSections={updateSections}
saveCount={saveCount}
/>
)
})}
<button
onClick={() => setSaveCount(prev => prev + 1)}
>State dependant update {saveCount}</button>
</>)
}
// Child.jsx
import React, {useEffect, useState, forwardRef, useImperativeHandle} from 'react'
export const Child = forwardRef((props, ref) => {
const {section, updateSections, saveCount} = props
const [statusState, setStatusState] = useState(section.status)
function modData() {
const obj = {
header: section.header,
status: statusState
}
updateSections(obj)
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) modData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
return (<>
<span style={{color: section.header}}>
header: {section.header}
</span>
<span>status: {section.status}</span>
<input
defaultValue={section.status}
onChange={(e) => setStatusState(e.target.value)}
/>
________________________________________
</>)
})
Child.displayName = 'MyChild';

Calls to setState not executing in proper order [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 12 months ago.
I am having issues with (I think) the timing of the sequential calls to setState variables... specifically in my 'handleStartOverClick()' call. This function makes two 'setState' calls, builds a list, then selects a random item from that list. However, my console.logs in that method are showing that nothing is actually getting cleared, and the new list isn't getting built. I am pretty sure this isn't the best 'react way' to handle the synchronous nature of these calls, but I just haven't been able to find a way to properly handle from the docs/otherwise.
So far I've tried wrapping in useCallback(), making the methods async to use .then, and even brute forcing a timeout in between calls just to see it work. But no luck.
Also, I've included the json that is being pulled in with the import of 'questionsJson' at the top, even though that is maybe an extraneous detail.
import React, { useEffect, useMemo, useState } from 'react';
import { Grid } from '#material-ui/core';
import questionsJson from 'assets/data/questions.json';
import { Question } from 'domains/core/models';
export default function Questions(): ReactElement {
const [leftOptionSelected, setLeftOptionSelected] = useState(false);
const [rightOptionSelected, setRightOptionSelected] = useState(false);
const [mainQuestionList, setMainQuestionList] = useState<Question[]>([]);
const [currentQuestion, setCurrentQuestion] = useState<Question | undefined>(undefined);
const [questionsLoaded, setQuestionsLoaded] = useState(false);
const removeQuestionFromMainQuestionList = useMemo(() => (questionToRemove: Question) => {
const updatedQuestionList = mainQuestionList.filter(question => question?.id !== questionToRemove?.id);
setMainQuestionList(updatedQuestionList);
},[setMainQuestionList, mainQuestionList]);
const getRandomQuestion = useMemo(() => () => {
for (let index = 0; index < mainQuestionList.length; index++) {
const numberOfQuestions = mainQuestionList.length;
const randomArrayPosition = Math.floor(Math.random() * numberOfQuestions);
const randomQuestion = mainQuestionList[randomArrayPosition];
setCurrentQuestion({
id: randomQuestion.id,
option1: randomQuestion.option1,
option2: randomQuestion.option2
});
removeQuestionFromMainQuestionList(randomQuestion);
console.log("new main question list", mainQuestionList)
}
},[mainQuestionList, removeQuestionFromMainQuestionList]);
const buildQuestionList = () => {
const questionList: Question[] = [];
const questionInfo = questionsJson.questionInfo;
for (let index = 0; index < questionInfo.length; index++) {
const currentQuestionSet = questionInfo[index];
let questions = currentQuestionSet.questions;
const questionLimit = currentQuestionSet.questionLimit;
for (let index = 0; index < questionLimit; index++) {
const numberOfQuestions = questions.length;
const randomArrayPosition = Math.floor(Math.random() * numberOfQuestions);
const questionToAdd = questions[randomArrayPosition];
questionList.push(questionToAdd);
questions = questions.filter(question => question?.id !== questionToAdd?.id);
}
}
setMainQuestionList(questionList);
console.log("initial question list", questionList);
setQuestionsLoaded(true);
}
useEffect(() => {
buildQuestionList();
}, []);
useEffect(() => {
if (questionsLoaded){
getRandomQuestion();
}
}, [questionsLoaded]);
const handleStartOverClick = () => {
console.log("in handle start over")
setCurrentQuestion(undefined);
console.log("current question", currentQuestion);
setMainQuestionList([]);
console.log("mainQuestionList", mainQuestionList);
buildQuestionList();
console.log("mainQuestionList", mainQuestionList);
setTimeout( () => {
getRandomQuestion();
console.log("current question after clearout", currentQuestion);
}, 4000);
}
const handleOptionClick = (sideClicked: string) => {
if (sideClicked == "left"){
setLeftOptionSelected(true);
setTimeout( () => {
setLeftOptionSelected(false);
getRandomQuestion();
}, 2000);
} else {
setRightOptionSelected(true);
setTimeout( () => {
setRightOptionSelected(false)
getRandomQuestion();
}, 2000);
}
}
return (
<Grid container direction="row" spacing={6}>
<Grid style={{ backgroundColor: "yellow"}} item xs={6}>
<div onClick={() => handleOptionClick("left")} style={{ pointerEvents: rightOptionSelected ? 'none' : 'auto'}}>
<div>{currentQuestion?.option1}</div>
</div>
</Grid>
<Grid style={{ backgroundColor: "green"}} item xs={6}>
<div onClick={() => handleOptionClick("right")} style={{ pointerEvents: leftOptionSelected ? 'none' : 'auto'}}>
<span></span>
<div style={{ backgroundColor: "green"}}>{currentQuestion?.option2}</div>
</div>
</Grid>
<Grid item xs={2}>
<button onClick={() => handleStartOverClick()}>
START OVER
</button>
</Grid>
</Grid>
);
}
{
"questionInfo":[
{
"category":"fun",
"questionLimit":2,
"questions":[
{
"id":1,
"option1":"Apple pie",
"option2":"Cherry pie"
},
{
"id":2,
"option1":"Jack",
"option2":"Jill"
}
]
},
{
"category":"lifestyle",
"questionLimit":2,
"questions":[
{
"id":3,
"option1":"Workout",
"option2":"Watch tv"
},
{
"id":4,
"option1":"Day",
"option2":"Night"
},
{
"id":5,
"option1":"Watch",
"option2":"Read"
}
]
},
{
"category":"interest",
"questionLimit":2,
"questions":[
{
"id":6,
"option1":"Cars",
"option2":"Baseball"
},
{
"id":7,
"option1":"Money",
"option2":"Fame"
}
]
}
]
}
Never set state and use the state value in the same function. Set state is async and will always console.log stale data in the same function.
simple solution:
const [state, setState] = useState({id: 0})
const someFunction = () => {
const newState = {id: 1}
setState(newState)
//console.log(state) NO! Stale data
console.log(newState) //New data. Use this instead of state inside this function
}
useEffect(() => {
console.log(state) //New, updated value
}, [state]) //Only console log if new state is changed.
From your implementation,
I can suggest you this way
Instead of using this
useEffect(() => {
if (questionsLoaded){
getRandomQuestion();
}
}, [questionsLoaded]);
You should change it to this
useEffect(() => {
if (mainQuestionList && mainQuestionList.length > 0){
getRandomQuestion();
}
}, [mainQuestionList]);
it means whenever you modify the question list, we will get the random question again.
After this change, you can get rid of questionsLoaded.
And in handleStartOverClick, you can remove the call of getRandomQuestion with setTimeout (buildQuestionList will help you to handle that case with useEffect after getting question list).

React Typewriter effect doesn't reset

I have created a typewriting effect with React and it works perfectly fine. However, when I change the language with i18n both texts don't have the same length and it keeps writing until both texts have the same length and then it changes the language and starts the effect again.
How can I reset the input when the language has changed? How can I reset the input when the component has been destroyed?
I have recorded a video
I have the same issue when I change from one page to another, as both pages have different texts and they don't have the same length.
Here code of my component
export const ConsoleText = ({text, complete = false}) => {
const [currentText, setCurrentText] = useState("");
const translatedText = i18n.t(text);
const index = useRef(0);
useEffect(() => {
if (!complete && currentText.length !== translatedText.length) {
const timeOut = setTimeout(() => {
setCurrentText((value) => value + translatedText.charAt(index.current));
index.current++;
}, 20);
return () => {
clearTimeout(timeOut);
}
} else {
setCurrentText(translatedText);
}
}, [translatedText, currentText, complete]);
return (
<p className="console-text">
{currentText}
</p>
);
};
You are telling react to do setCurrentText(translatedText) only when it is complete or when the compared text lengths are equal, so yes it continues to write until this moment.
To reset your text when text changes, try creating another useEffect that will reset your states :
useEffect(() => {
index.current = 0;
setCurrentText('');
}, [text]);
Now, I actually did this exact same feature few days ago, here is my component if it can help you :
import React from 'react';
import DOMPurify from 'dompurify';
import './text-writer.scss';
interface ITextWriterState {
writtenText: string,
index: number;
}
const TextWriter = ({ text, speed }: { text: string, speed: number }) => {
const initialState = { writtenText: '', index: 0 };
const sanitizer = DOMPurify.sanitize;
const [state, setState] = React.useState<ITextWriterState>(initialState);
React.useEffect(() => {
if (state.index < text.length - 1) {
const animKey = setInterval(() => {
setState(state => {
if (state.index > text.length - 1) {
clearInterval(animKey);
return { ...state };
}
return {
writtenText: state.writtenText + text[state.index],
index: state.index + 1
};
});
}, speed);
return () => clearInterval(animKey);
}
}, []);
// Reset the state when the text is changed (Language change)
React.useEffect(() => {
if (text.length > 0) {
setState(initialState);
}
}, [text])
return <div className="text-writer-component"><span className="text" dangerouslySetInnerHTML={{ __html: sanitizer(state.writtenText) }} /></div>
}
export default TextWriter;
The translation is made outside of the component so you can pass any kind of text to the component.

Rerender only occurs on second state change,

I am hoping you guys can help me,
I think I may be missing a simple concept. I have a Component within that there is another component that has an array that is prop drilled from its parent component. In the Child component, the list is then mapped and displayed.
Now the issue is when I update the state of the Array, ie add another item to the Array using SetArray([...array, newItem]),
the useEffect in the ChildComponent will console.log the new array but the actual display does not change until I add another element to the array.
When I add another element the first element I added appears but the 2nd one doesn't.
Hopefully, that makes some sense
ChildComponent:
import React, { useState, useEffect } from "react";
////EDITOR//// List
import { Grid, Button, Card } from "#material-ui/core";
import Timestamp from "./Timestamp";
const TimestampList = ({ setTime, match, setMatchEdit, render }) => {
const [timestamps, setTimestamps] = useState([]);
useEffect(() => {
const setInit = async () => {
try {
console.log(match);
const m = await match.scores.map(player => {
console.log(player);
if (player.totalScores) {
return player.totalScores;
}
});
console.log(m);
if (m[0] && m[1]) {
setTimestamps(
[...m[0], ...m[1]].sort((a, b) => {
return a.time - b.time;
})
);
}
if (m[0] && !m[1]) {
setTimestamps(
m[0].sort((a, b) => {
return a.time - b.time;
})
);
}
if (m[1] && !m[0]) {
setTimestamps(
m[1].sort((a, b) => {
return a.time - b.time;
})
);
}
} catch (error) {
console.log(error);
}
};
if (match) {
setInit();
}
console.log(match);
}, [match]);
return (
<Grid
component={Card}
style={{ width: "100%", maxHeight: "360px", overflow: "scroll" }}
>
{timestamps && timestamps.map(timestamp => {
console.log(timestamp);
const min = Math.floor(timestamp.time / 60);
const sec = timestamp.time - min * 60;
const times = `${min}m ${sec}sec`;
return (
<Timestamp
time={times}
pointsScored={timestamp.points}
/>
);
})}
<Grid container direction='row'></Grid>
</Grid>
);
};
export default TimestampList;

Select row on click react-table

I am trying to find the best table to use with my react apps, and for now, the react-table offers everything I need (pagination, server-side control, filtering, sorting, footer row).
This being said, I can't seem to be able to select a row. There are no examples that show this.
Some things, that I have tried include trying to set a className on click of the row. But I can't seem to find the calling element in e nor t. Also, I don't like this approach, because it is not how a react app should do things.
<ReactTable
...
getTrProps={(state, rowInfo, column, instance) => {
return {
onClick: (e, t) => {
t.srcElement.classList.add('active')
},
style: {
}
}
}}
/>
Some possible workaround would be to render checkboxes as a first column, but this is not optimal as it limits the area to click to 'activate' the row. Also, the visual feedback will be less expressive.
Am I missing the elephant in the room? And if not, do you know another library that supports the things that I've described earlier?
Thank you!
EDIT:
Another option, this being open source, is to suggest an edit. And maybe this is the proper thing to do.
EDIT 2
Another thing, suggested by Davorin Ruševljan in the comments, but I couldn't make it work was:
onRowClick(e, t, rowInfo) {
this.setState((oldState) => {
let data = oldState.data.slice();
let copy = Object.assign({}, data[rowInfo.index]);
copy.selected = true;
copy.FirstName = "selected";
data[rowInfo.index] = copy;
return {
data: data,
}
})
}
....
getTrProps={(state, rowInfo, column) => {
return {
onClick: (e, t) => { this.onRowClick(e, t, rowInfo) },
style: {
background: rowInfo && rowInfo.row.selected ? 'green' : 'red'
}
}
}}
This sets the 'FirstName' column to 'selected', but does not set the class to 'green'
I found the solution after a few tries, I hope this can help you. Add the following to your <ReactTable> component:
getTrProps={(state, rowInfo) => {
if (rowInfo && rowInfo.row) {
return {
onClick: (e) => {
this.setState({
selected: rowInfo.index
})
},
style: {
background: rowInfo.index === this.state.selected ? '#00afec' : 'white',
color: rowInfo.index === this.state.selected ? 'white' : 'black'
}
}
}else{
return {}
}
}
In your state don't forget to add a null selected value, like:
state = { selected: null }
There is a HOC included for React-Table that allows for selection, even when filtering and paginating the table, the setup is slightly more advanced than the basic table so read through the info in the link below first.
After importing the HOC you can then use it like this with the necessary methods:
/**
* Toggle a single checkbox for select table
*/
toggleSelection(key: number, shift: string, row: string) {
// start off with the existing state
let selection = [...this.state.selection];
const keyIndex = selection.indexOf(key);
// check to see if the key exists
if (keyIndex >= 0) {
// it does exist so we will remove it using destructing
selection = [
...selection.slice(0, keyIndex),
...selection.slice(keyIndex + 1)
];
} else {
// it does not exist so add it
selection.push(key);
}
// update the state
this.setState({ selection });
}
/**
* Toggle all checkboxes for select table
*/
toggleAll() {
const selectAll = !this.state.selectAll;
const selection = [];
if (selectAll) {
// we need to get at the internals of ReactTable
const wrappedInstance = this.checkboxTable.getWrappedInstance();
// the 'sortedData' property contains the currently accessible records based on the filter and sort
const currentRecords = wrappedInstance.getResolvedState().sortedData;
// we just push all the IDs onto the selection array
currentRecords.forEach(item => {
selection.push(item._original._id);
});
}
this.setState({ selectAll, selection });
}
/**
* Whether or not a row is selected for select table
*/
isSelected(key: number) {
return this.state.selection.includes(key);
}
<CheckboxTable
ref={r => (this.checkboxTable = r)}
toggleSelection={this.toggleSelection}
selectAll={this.state.selectAll}
toggleAll={this.toggleAll}
selectType="checkbox"
isSelected={this.isSelected}
data={data}
columns={columns}
/>
See here for more information:
https://github.com/tannerlinsley/react-table/tree/v6#selecttable
Here is a working example:
https://codesandbox.io/s/react-table-select-j9jvw
I am not familiar with, react-table, so I do not know it has direct support for selecting and deselecting (it would be nice if it had).
If it does not, with the piece of code you already have you can install the onCLick handler. Now instead of trying to attach style directly to row, you can modify state, by for instance adding selected: true to row data. That would trigger rerender. Now you only have to override how are rows with selected === true rendered. Something along lines of:
// Any Tr element will be green if its (row.age > 20)
<ReactTable
getTrProps={(state, rowInfo, column) => {
return {
style: {
background: rowInfo.row.selected ? 'green' : 'red'
}
}
}}
/>
if u want to have multiple selection on select row..
import React from 'react';
import ReactTable from 'react-table';
import 'react-table/react-table.css';
import { ReactTableDefaults } from 'react-table';
import matchSorter from 'match-sorter';
class ThreatReportTable extends React.Component{
constructor(props){
super(props);
this.state = {
selected: [],
row: []
}
}
render(){
const columns = this.props.label;
const data = this.props.data;
Object.assign(ReactTableDefaults, {
defaultPageSize: 10,
pageText: false,
previousText: '<',
nextText: '>',
showPageJump: false,
showPagination: true,
defaultSortMethod: (a, b, desc) => {
return b - a;
},
})
return(
<ReactTable className='threatReportTable'
data= {data}
columns={columns}
getTrProps={(state, rowInfo, column) => {
return {
onClick: (e) => {
var a = this.state.selected.indexOf(rowInfo.index);
if (a == -1) {
// this.setState({selected: array.concat(this.state.selected, [rowInfo.index])});
this.setState({selected: [...this.state.selected, rowInfo.index]});
// Pass props to the React component
}
var array = this.state.selected;
if(a != -1){
array.splice(a, 1);
this.setState({selected: array});
}
},
// #393740 - Lighter, selected row
// #302f36 - Darker, not selected row
style: {background: this.state.selected.indexOf(rowInfo.index) != -1 ? '#393740': '#302f36'},
}
}}
noDataText = "No available threats"
/>
)
}
}
export default ThreatReportTable;
The answer you selected is correct, however if you are using a sorting table it will crash since rowInfo will became undefined as you search, would recommend using this function instead
getTrGroupProps={(state, rowInfo, column, instance) => {
if (rowInfo !== undefined) {
return {
onClick: (e, handleOriginal) => {
console.log('It was in this row:', rowInfo)
this.setState({
firstNameState: rowInfo.row.firstName,
lastNameState: rowInfo.row.lastName,
selectedIndex: rowInfo.original.id
})
},
style: {
cursor: 'pointer',
background: rowInfo.original.id === this.state.selectedIndex ? '#00afec' : 'white',
color: rowInfo.original.id === this.state.selectedIndex ? 'white' : 'black'
}
}
}}
}
If you are using the latest version (7.7 at the time) it is possible to select rows using toggleRoWSelected() see full example;
<tr
{...row.getRowProps()}
className="odd:bg-white even:bg-gray-100"
onClick={() => row.toggleRowSelected()}
>
{row.cells.map((cell) => {
return (
<td {...cell.getCellProps()} className="p-2">
{cell.render("Cell")}
</td>
);
})}
</tr>;
Another mechanism for dynamic styling is to define it in the JSX for your component. For example, the following could be used to selectively style the current step in the React tic-tac-toe tutorial (one of the suggested extra credit enhancements:
return (
<li key={move}>
<button style={{fontWeight:(move === this.state.stepNumber ? 'bold' : '')}} onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
Granted, a cleaner approach would be to add/remove a 'selected' CSS class but this direct approach might be helpful in some cases.
Multiple rows with checkboxes and select all using useState() hooks. Requires minor implementation to adjust to own project.
const data;
const [ allToggled, setAllToggled ] = useState(false);
const [ toggled, setToggled ] = useState(Array.from(new Array(data.length), () => false));
const [ selected, setSelected ] = useState([]);
const handleToggleAll = allToggled => {
let selectAll = !allToggled;
setAllToggled(selectAll);
let toggledCopy = [];
let selectedCopy = [];
data.forEach(function (e, index) {
toggledCopy.push(selectAll);
if(selectAll) {
selectedCopy.push(index);
}
});
setToggled(toggledCopy);
setSelected(selectedCopy);
};
const handleToggle = index => {
let toggledCopy = [...toggled];
toggledCopy[index] = !toggledCopy[index];
setToggled(toggledCopy);
if( toggledCopy[index] === false ){
setAllToggled(false);
}
else if (allToggled) {
setAllToggled(false);
}
};
....
Header: state => (
<input
type="checkbox"
checked={allToggled}
onChange={() => handleToggleAll(allToggled)}
/>
),
Cell: row => (
<input
type="checkbox"
checked={toggled[row.index]}
onChange={() => handleToggle(row.index)}
/>
),
....
<ReactTable
...
getTrProps={(state, rowInfo, column, instance) => {
if (rowInfo && rowInfo.row) {
return {
onClick: (e, handleOriginal) => {
let present = selected.indexOf(rowInfo.index);
let selectedCopy = selected;
if (present === -1){
selected.push(rowInfo.index);
setSelected(selected);
}
if (present > -1){
selectedCopy.splice(present, 1);
setSelected(selectedCopy);
}
handleToggle(rowInfo.index);
},
style: {
background: selected.indexOf(rowInfo.index) > -1 ? '#00afec' : 'white',
color: selected.indexOf(rowInfo.index) > -1 ? 'white' : 'black'
},
}
}
else {
return {}
}
}}
/>
# react-table with edit button #
const [rowIndexState, setRowIndexState] = useState(null);
const [rowBackGroundColor, setRowBackGroundColor] = useState('')
{...row.getRowProps({
onClick: (e) => {
if (!e.target.cellIndex) {
setRowIndexState(row.index);
setRowBackGroundColor('#f4f4f4')
}
},
style: {
background: row.index === rowIndexState ? rowBackGroundColor : '',
},
})}

Resources