Accessing conditionally rendered canvas element in React for plotting - javascript

I'm relatively new to React and am stumped trying to integrate third party plotting libraries into my application, in this instance chartjs. (note: I have looked at similar questions on here but was unable to glean a solution from them)
I need to create an plot instance that targets the context of a HTML canvas element. I'm using React's ref prop to try to implement this behaviour but I think I'm being tripped up by the asynchronicity of React's methods. It seems that ctx.current is always null in the render() method. I've also tried placing the chart() call in componentDidMount but then this.ctx is undefined, presumably because the conditional rendering hasn't occurred yet so the ref doesn't exist?
Any help would be much appreciated!
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = {data: [], labels: []}
this.ctx = React.createRef();
this.fakeAPICall();
}
// retrieve fascinating data...
fakeAPICall = () => {
setTimeout(() => {
console.log('Data fetched');
this.setState({
data: [1,2,3],
labels: ['A', 'B', 'C']
})
}, 1500);
}
chart = () => {
console.log('creating chart')
const myChart = new Chart(this.ctx, {
type: 'bar',
data: {
labels: this.state.labels,
datasets: [{
label: 'Series 1',
data: this.state.data
}]
}
})
}
render() {
if(this.ctx && this.ctx.current) this.chart();
return (
<div>
<h1>Fascinating Chart</h1>
{
this.state.data.length
?
<canvas
ref={c => {
this.ctx = c.getContext('2d')}
}
style={{ width: 400, height: 200 }}
/>
:
<p>Loading ...</p>
}
</div>
);
}
}
ReactDOM.render(
<Hello name="World" />,
document.getElementById('container')
);
My current best attempt is attached here in a fiddle: https://jsfiddle.net/69z2wepo/327056/

It doesn't work because there are no further state updates after you assign the ref to the canvas; that means no more re-renderings (calls to render method) so you never get to execute this.chart(). Instead of calling that last method in render, add it to the setState callback in fakeAPICall:
this.setState({
data: [1,2,3],
labels: ['A', 'B', 'C']
}, () => this.chart());
The callback will be executed once setState is completed and the component is re-rendered. You can read more about state callbacks here.

Related

React.useMemo is re-rendering all items in array

I have a react component that stores a set of fruits in useState. I have a memoized function (visibleFruits) that filters fruits. I map visibleFruits to the dom.
The problem is, when i check a fruit, all visible fruits re-render.
I am expecting that only the selected one re-renders since it is the only one that is changing.
Is there a way for me to use this pattern but prevent all from re-rendering on check?
In real life, there is a complex function in visibleFruits useMemo. So I can't simply append the filter before the map.
Edit, here is updated edit:
const Example = () => {
const [fruits, setFruits] = React.useState([
{ id: 1, name: 'apple', visible: true, selected: false },
{ id: 2, name: 'banana', visible: false, selected: false },
{ id: 3, name: 'orange', visible: true, selected: false }
])
const visibleFruits = React.useMemo(() => {
return fruits.filter((f) => f.visible)
}, [fruits])
const handleCheck = (bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
f.selected = bool
}
return f
})
})
}
return (
<div>
{visibleFruits.map((fruit) => {
return <FruitOption fruit={fruit} handleCheck={handleCheck} />
})}
</div>
)
}
const FruitOption = ({ fruit, handleCheck }) => {
console.log('** THIS RENDERS TWICE EVERY TIME USER SELECTS A FRUIT **')
return (
<div key={fruit.id}>
<input
checked={fruit.selected}
onChange={(e) => handleCheck(e.target.checked, fruit.id)}
type='checkbox'
/>
<label>{fruit.name}</label>
</div>
)
}
export default Example
First, there's a problem with the handleCheck function (but it's not related to what you're asking about). Your code is modifying a fruit object directly (f.selected = bool), but you're not allowed to do that with React state, objects in state must not be directly modified, and rendering may not be correct if you break that rule. Instead, you need to copy the object and modify the copy (like you are with the array):
const handleCheck = (bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
return {...f, selected: bool}; // ***
}
return f;
});
});
};
But that's not what you're asking about, just something else to fix. :-)
The reason you see the console.log executed twice after handleCheck is that the component has to be re-rendered (for the change), and there are two visible fruits, so you see two calls to your FruitOption component function. There are two reasons for this:
handleChange changes every time your Example component function is called, so FruitOption sees a new prop every time; and
FruitOption doesn't avoid re-rendering when its props don't change, so even once you've fixed #1, you'd still see two console.log calls; and
Separately, there's no key on the FruitOption elements, which can cause rendering issues. Always include a meaningful key when rendering elements in an array. (Don't just use the index, it's problematic; but your fruit objects have an id, which is perfect.)
To fix it:
Memoize handleChange so that it's not recreated every time, probably via useCallback, and
Use React.memo so that FruitOption doesn't get called if its props don't change (see the end of this answer for the class component equivalent), and
Add a meaningful key to the FruitOption elements in Example
Taking those and the handleChange fix above and putting them all together:
const Example = () => {
const [fruits, setFruits] = React.useState([
{ id: 1, name: 'apple', visible: true, selected: false },
{ id: 2, name: 'banana', visible: false, selected: false },
{ id: 3, name: 'orange', visible: true, selected: false }
]);
const visibleFruits = React.useMemo(() => {
return fruits.filter((f) => f.visible);
}, [fruits]);
const handleCheck = React.useCallback(
(bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
return {...f, selected: bool}; // ***
}
return f;
});
});
},
[] // *** No dependencies since all it uses is `setFruits`, which is
// stable for the lifetime of the component
);
return (
<div>
{visibleFruits.map((fruit) => {
// *** Note the key
return <FruitOption key={fruit.id} fruit={fruit} handleCheck={handleCheck} />
})}
</div>
);
}
// *** `React.memo` will compare the props and skip the call if they're the same, reusing
// the previous call's result.
const FruitOption = React.memo(({ fruit, handleCheck }) => {
console.log(`Rendering fruit ${fruit.id}`);
return (
<div key={fruit.id}>
<input
checked={fruit.selected}
onChange={(e) => handleCheck(e.target.checked, fruit.id)}
type='checkbox'
/>
<label>{fruit.name}</label>
</div>
);
});
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
As you can see, with all that in place, only the changed fruit is re-rendered.
Re React.memo: For components with more complicated requirements, you can provide a function as a second argument that determines whether the two sets of props are "the same" for rendering purposes. By default, React.memo just does a shallow equality comparison, which is often sufficient.
Finally: For class components, the equivalent of React.memo without providing an equality callback is extending PureComponent instead of Component. If you want to make your check of the props more fine-grained, you can implement shouldComponentUpdate instead.

Prevent rerender on function prop update

I have a form with several layers of child components. The state of the form is maintained at the highest level and I pass down functions as props to update the top level. The only problem with this is when the form gets very large (you can dynamically add questions) every single component reloads when one of them updates. Here's a simplified version of my code (or the codesandbox: https://codesandbox.io/s/636xwz3rr):
const App = () => {
return <Form />;
}
const initialForm = {
id: 1,
sections: [
{
ordinal: 1,
name: "Section Number One",
questions: [
{ ordinal: 1, text: "Who?", response: "" },
{ ordinal: 2, text: "What?", response: "" },
{ ordinal: 3, text: "Where?", response: "" }
]
},
{
ordinal: 2,
name: "Numero dos",
questions: [
{ ordinal: 1, text: "Who?", response: "" },
{ ordinal: 2, text: "What?", response: "" },
{ ordinal: 3, text: "Where?", response: "" }
]
}
]
};
const Form = () => {
const [form, setForm] = useState(initialForm);
const updateSection = (idx, value) => {
const { sections } = form;
sections[idx] = value;
setForm({ ...form, sections });
};
return (
<>
{form.sections.map((section, idx) => (
<Section
key={section.ordinal}
section={section}
updateSection={value => updateSection(idx, value)}
/>
))}
</>
);
};
const Section = props => {
const { section, updateSection } = props;
const updateQuestion = (idx, value) => {
const { questions } = section;
questions[idx] = value;
updateSection({ ...section, questions });
};
console.log(`Rendered section "${section.name}"`);
return (
<>
<div style={{ fontSize: 18, fontWeight: "bold", margin: "24px 0" }}>
Section name:
<input
type="text"
value={section.name}
onChange={e => updateSection({ ...section, name: e.target.value })}
/>
</div>
<div style={{ marginLeft: 36 }}>
{section.questions.map((question, idx) => (
<Question
key={question.ordinal}
question={question}
updateQuestion={v => updateQuestion(idx, v)}
/>
))}
</div>
</>
);
};
const Question = props => {
const { question, updateQuestion } = props;
console.log(`Rendered question #${question.ordinal}`);
return (
<>
<div>{question.text}</div>
<input
type="text"
value={question.response}
onChange={e =>
updateQuestion({ ...question, response: e.target.value })
}
/>
</>
);
};
I've tried using useMemo and useCallback, but I can't figure out how to make it work. The problem is passing down the function to update its parent. I can't figure out how to do that without updating it every time the form updates.
I can't find a solution online anywhere. Maybe I'm searching for the wrong thing. Thank you for any help you can offer!
Solution
Using Andrii-Golubenko's answer and this article React Optimizations with React.memo, useCallback, and useReducer I was able to come up with this solution:
https://codesandbox.io/s/myrjqrjm18
Notice how the console log only shows re-rendering of components that have changed.
Use React feature React.memo for functional components to prevent re-render if props not changed, similarly to PureComponent for class components.
When you pass callback like that:
<Section
...
updateSection={value => updateSection(idx, value)}
/>
your component Section will rerender each time when parent component rerender, even if other props are not changed and you use React.memo. Because your callback will re-create each time when parent component renders. You should wrap your callback in useCallback hook.
Using useState is not a good decision if you need to store complex object like initialForm. It is better to use useReducer;
Here you could see working solution: https://codesandbox.io/s/o10p05m2vz
I would suggest using life cycle methods to prevent rerendering, in react hooks example you can use, useEffect. Also centralizing your state in context and using the useContext hook would probably help as well.
laboring through this issue with a complex form, the hack I implemented was to use onBlur={updateFormState} on the component's input elements to trigger lifting form data from the component to the parent form via a function passed as a prop to the component.
To update the component's input elelment, I used onChange={handleInput} using a state within the compononent, which component state was then passed ot the lifting function when the input (or the component as a whole, if there's multiple input field in the component) lost focus.
This is a bit hacky, and probably wrong for some reason, but it works on my machine. ;-)

Conditional creation of an object

I am working with Chartjs and trying to create the chart object once within a function. Then skipping that piece of code in the function so it will just update the chart on future calls.
I am not not sure how to structure the conditional chart object creation. Below is my code approach ( it does not run atm).
class BtcOverview extends React.Component {
constructor(props) {
super(props);
this.canvasRef = React.createRef();
}
componentDidMount () {
this.callApi();
}
callApi = () => {
fetch(getApi())
.then(results => results.json())
.then(marketData => {
//API FETCH STUFF
const chartOptions = {
...{}
...this.props.chartOptions
};
const BtcChart = new Chart(this.canvasRef.current, {
type: "LineWithLine",
data: this.props.chartData,
options: chartOptions
});
BtcChart.render();
// I want to create the BtcChart and render() on the first run. But then after that only call update() on the chart object
})
}
render() {
return (
<>
<CardBody className="pt-2">
<canvas
height="160"
ref={this.canvasRef}
style={{ maxWidth: "100% !important" }} className="mb-1" />
</CardBody>
</Card>
</>
);
}
}
Update: I should explain a bit more clearly. I want to be be able to create a 'new Chart' once and then on subsequent runs rather call BtcChart update given the chart object already exists.
When I tried to use an if-else statement to achieve that it would not compile because I guess because the chart BtcChart has not been created at the time of compiling and possibly a scope issue. So I am have been trying hacks that are probably wrong in their approach.

When SectionList/Flatlist is scrolling/rendering items UI thread seems blocked (React Native)

We use Sectionlist and Flatlist in our react native project and we use websocket to receive/update data.
Every time we receive data via websocket we want to update some information in our Sectionlist/ Flatlist, but we have more than 100 row so when the list trying to re-render UI thread seems blocked. Therefore, the onPress function delay for 3~5 seconds.
So my questions are:
Does Flastlist/Sectionlist re-render the whole list when it's updating or it only re-render specific rows? (Because we only update some information in some specific rows not every rows have new information to update)
If it re-render the whole list, how I can let Flastlist or Sectionlist only re-render those specific rows?
If it only re-render specific rows, why the UI still seem blocked? How can I prevent this situation?
Or maybe the problem is not about the UI block? Maybe there are some other reasons why the onPress function delay? If so, what are the problems and how can I fix that?
class RowItem1 extends React.Component{
constructor(props){
super(props);
}
shouldComponentUpdate = (nextProps, nextState) => {
if (nextProps.item == this.props.item) {
return false;
}
return true;
};
render=()=>{
return (
<View>
<TouchableWithoutFeedback
onPress={() => { consle.log(1234) }}
>
</TouchableWithoutFeedback>
<View>
<Text>{this.props.item.text}</Text>
<Text>{this.props.item.name}</Text>
</View>
</View>
)
}
}
export default class main extends React.Component {
constructor(props) {
super(props);
}
componentWillMount = () => {
var ws = new WebSocket('ws://123456');
ws.onmessage = function (evt) {
let data = JSON.parse(evt.data);
this.setState({
A: data.A,
B: data.B,
C: data.C
});
};
};
getItemLayout = (data, index) => ({ length: 74, offset: 74 * index, index });
renderA = ({ item, index, section }) => {
return <RowItem1 item={item}/>
}
render() {
return (
<View>
<SectionList
sections={[
{
title: "A",
data: this.state.A,
renderItem: this.renderA,
},
{
title: "B",
data: this.state.B,
renderItem: this.renderB,
},
{
title: "C",
data: this.state.C,
renderItem: this.renderC,
}
]}
initialNumToRender={10}
removeClippedSubviews={true}
getItemLayout={this.getItemLayout}
keyExtractor={(item, index) => item + index}
extraData={this.state}
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
/>
</View>
);
}
}
The First time:
data = {A:[{'text': 0, 'name': 'A'},{'text': 0, 'name': 'B'}, {'text': 0, 'name':'C'}]}
The Second time:
data = {A:[{'text': 3, 'name': 'A'},{'text': 0, 'name': 'B'}, {'text': 0, 'name':'C'}]}
Compare the two data I received, I only want to re-render the first row of the list because there is only the first row of data got updated. How can I do that?
I know this is really late, but for those of you still having this issue, you can fix it by setting the scrollEventThrottle property to something really high, like 1000. I should note that this will really only work if you don't need the onScroll event.
<FlatList data={data} renderItem={renderItem} scrollEventThrottle={1000} />

Toggling visibility of array of stateless react components

I am trying to simply map over some data returned from an api and create a stateless component for each object returned. I want to be able to click on any of the components to toggle visibility of the rest of its data.
I have tried numerous ways to do it and keep hitting a brick wall, i've also scoured stack overflow and cannot seem to find an answer.
I have gotten it working by making them individual class components, however it seems like a lot of unnecessary code for just a toggle functionality.
Thank you in advance for any help or insight, here is a quick breakdown of what I have currently.
For clarification this is a simple app for me to learn about using react and an external api, it is not using redux.
fetched users in state of class component
class PersonList extends Component {
constructor(props) {
super(props);
this.state = {
resource: []
};
}
async componentDidMount() {
let fetchedData = await API_Call("people");
this.setState({ resource: fetchedData.results });
while (fetchedData.next) {
let req = await fetch(fetchedData.next);
fetchedData = await req.json();
this.setState({
resource: [...this.state.resource, ...fetchedData.results]
});
}
}
}
Then map over the results and render a component for each result
render() {
const mappedPeople = this.state.resource.map((person, i) => (
<Person key={i} {...person} />
));
return <div>{mappedPeople}</div>;
}
Is there i can make each person component a stateless component with the ability to click on it and display the rest of the data? Here is what I have currently.
class Person extends Component {
constructor(props) {
super(props);
this.state = {
visibility: false
};
}
toggleVisible = () => {
this.setState(prevState => ({
visibility: !prevState.visibility
}));
};
render() {
return (
<div>
<h1 onClick={this.toggleVisible}>{this.props.name}</h1>
{this.state.visibility && (
<div>
<p>{this.props.height}</p>
</div>
)}
</div>
);
}
}
Again thanks in advance for any insight or help!
You could keep an object visible in your parent component that will have keys representing a person index and a value saying if the person is visible or not. This way you can toggle the person's index in this single object instead of having stateful child components.
Example
class PersonList extends Component {
constructor(props) {
super(props);
this.state = {
resource: [],
visible: {}
};
}
// ...
toggleVisibility = index => {
this.setState(previousState => {
const visible = { ...previousState.visibile };
visible[index] = !visible[index];
return { visible };
});
};
render() {
const mappedPeople = this.state.resource.map((person, i) => (
<Person
key={i}
{...person}
visible={this.state.visible[i]}
onClick={() => this.toggleVisibility(i)}
/>
));
return <div>{mappedPeople}</div>;
}
}
const Person = (props) => (
<div>
<h1 onClick={props.onClick}>{props.name}</h1>
{props.visible && (
<div>
<p>{props.height}</p>
</div>
)}
</div>
);
Similar idea with #Tholle but a different approach. Assuming there is an id in the person object we are changing visibles state and toggling ids.
class PersonList extends React.Component {
constructor(props) {
super(props)
this.state = {
resource: this.props.persons,
visibles: {},
}
}
toggleVisible = id => this.setState( prevState => ({
visibles: { ...prevState.visibles, [id]: !prevState.visibles[id] },
}))
render() {
const mappedPeople =
this.state.resource.map((person, i) =>
<Person
key={person.id}
visibles={this.state.visibles}
toggleVisible={this.toggleVisible}
{...person}
/>
)
return (
<div>
{mappedPeople}
</div>
)
}
}
const Person = (props) => {
const handleVisible = () =>
props.toggleVisible( props.id );
return (
<div>
<h1 onClick={handleVisible}>
{props.name}</h1>
{props.visibles[props.id] &&
<div>
<p>{props.height}</p>
</div>
}
</div>
);
}
const persons = [
{ id: 1, name: "foo", height: 10 },
{ id: 2, name: "bar", height: 20 },
{ id: 3, name: "baz", height: 30 },
]
const rootElement = document.getElementById("root");
ReactDOM.render(<PersonList persons={persons} />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
You can make sure your "this.state.resource" array has a visibility flag on each object:
this.state.resource = [
{ ..., visibility: true },
{ ..., visibility: false}
...
];
Do this by modifying your fetch a little bit.
let fetchedData = await API_Call("people");
this.setState({
resource: fetchedData.results.map(p => ({...p, visiblity: true}))
});
Merge your Person component back into PersonList (like you are trying to do), and on your onclick, do this:
onClick={() => this.toggleVisible(i)}
Change toggleVisible() function to do the following.
toggleVisible = (idx) => {
const personList = this.state.resource;
personList[idx].visibility = !personList[idx].visibility;
this.setState({ resource: personList });
}
So now, when you are doing:
this.state.resource.map((person, i) => ...
... you have access to "person.visibility" and your onclick will toggle the particular index that is clicked.
I think that directly answers your question, however...
I would continue with breaking out Person into it's own component, it really is good practice!
Other than better organization, one of the main reason is to avoid lamdas in props (which i actually did above). Since you need to do an onClick per index, you either need to use data attributes, or actually use React.Component for each person item.
You can research this a bit here:
https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md
BTW you can still create "components" that aren't "React.Component"s like this:
import React from 'react';
const Person = ({ exProp1, exProp2, exProp3}) => {
return <div>{exProp1 + exProp2 + exProp3}</div>
}
Person.propTypes = {
...
}
export default Person;
As you can see, nothing is inheriting from React.Component, so you are getting the best of both worlds (create components without creating "Components"). I would lean towards this approach, vs putting everything inline. But if your application is not extremely large and you just want to get it done, going with the first approach isn't terribly bad.

Categories