React native performance issue - reactjs

I am using coincap api's to first fetch Data of about 1500+ crypto currency and then Web-socket to update the updated value of crypto Currency.
I a using redux to manage my state here
Inside My componentDidMount(), I am calling a redux action fetchCoin which fetches the value of the coin
componentDidMount() {
this.props.fetchCoin()
}
And then In return I am doing something like this
<FlatList
data={this.state.searchCoin ? displaySearchCrypto : this.props.cryptoLoaded}
renderItem={({ item }) => (
<CoinCard
key={item["short"]}
coinShortName = {item["short"]}
coinName = {item["long"]}
coinPrice = {item["price"].toFixed(2)}
percentChange = {item["perc"].toFixed(2)}
/>
Then I have a web-socket which updates the value of cryptocurrency like this
componentDidUpdate() {
if (this.state.updateCoinData || this.updateCoinData.length < 1 ) {
this.updateCoinData = [...this.props.cryptoLoaded];
this.setState({updateCoinData: true})
}
this.socket.on('trades', (tradeMsg) => {
for (let i=0; i< this.updateCoinData.length; i++) {
if (this.updateCoinData[i]["short"] == tradeMsg.coin ) {
//Search for changed Crypto Value
this.updateCoinData[i]["perc"] = tradeMsg["message"]["msg"]["perc"]
this.updateCoinData[i]["price"] = tradeMsg['message']['msg']['price']
//Update the crypto Value state in Redux
this.props.updateCrypto(this.updateCoinData);
}
}
})
}
Now, While this work, the problem is that this is slowing my app like hell since whenever the socket sends new data, it has to render every component and hence events like touch and search takes lot of time to execute. [Update] It turns out my app is rendering something even If i remove socket connection, check out update 2
[Question:] What should I do so that I can improve the performance of App? (Something like not using state or using DOM to update my app and so on).
[Update 1:] I am using https://github.com/irohitb/Crypto
And these two are js files where all the logic is happening
https://github.com/irohitb/Crypto/blob/master/src/container/cryptoContainer.js
https://github.com/irohitb/Crypto/blob/master/src/components/CoinCard.js
I have also move from map to Flatlist.
[Update: 2] I found that there are endless render happening inside my App which is probably keeping my thread busy (I mean it is endless & unnecessarily passing props). I asked the same question on separate Stackoverflow thread but didn't received a proper response and since it is related to performance, I thought about putting a bounty on it here.
Please check this thread: infinite Render in React
[Answer Update:] While there are many great answers here, Just in case someone wants to understand how it worked, You could probably clone my repository and go back to before this commit. I have linked the commit to the point where my problems was solved (so you might need to go back and see what I was doing wrong). Also, All the answers were very useful and not hard to comprehend so you should definitely go through them.

Each time your component updates it starts a new socket which results in a memory leak and will cause this.props.updateCrypto(updateCoinData); to be called multiple times for the same data. This can be fixed by opening the socket in componentDidMount() and closing it in componentWillUnmount().
You can also buffer multiple record updates and change the FlatList data in one go every couple of seconds.
Edit, working example (App.js):
import React, { Component } from 'react';
import { Text, View, FlatList } from 'react-native';
import SocketIOClient from 'socket.io-client';
type Props = {};
export default class App extends Component<Props> {
constructor(props) {
super(props);
this.currencies = {};
this.state = {
currenciesList: [],
}
}
componentDidMount() {
this.socket = SocketIOClient('https://coincap.io');
this.socket.on('trades', (tradeMsg) => {
const time = new Date();
// Store updates to currencies in an object
this.currencies[tradeMsg.message.msg.short] = {
...tradeMsg.message.msg,
time: time.getHours() + ':' + time.getMinutes() + ':' + time.getSeconds(),
};
// Create a new array from all currencies
this.setState({currenciesList: Object.values(this.currencies)})
});
}
componentWillUnmount() {
this.socket.disconnect();
}
render() {
return (
<FlatList
data={this.state.currenciesList}
extraData={this.state.currenciesList}
keyExtractor={(item) => item.short}
renderItem={({item}) => <View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
<Text style={{flex: 1}}>{item.time}</Text>
<Text style={{flex: 1}}>{item.short}</Text>
<Text style={{flex: 1}}>{item.perc}</Text>
<Text style={{flex: 1}}>{item.price}</Text>
</View>}
/>
);
}
}

There're many standard ways to improve react app performance, the most common:
use usual react optimizations (shouldComponentUpdate, PureComponent - read docs)
use virtual lists (limit visible parts of data)
In this case I would add:
Don't process data before optimizations - f.e. formatting data that didn't changed is at least unnecessary. You can insert intermediate component (optimization layer) that will pass/update formatted data into <CoinCard /> only on 'raw data' change.
You might not need Redux at all (store data in state) when data is used in one place/simple structure. Of course you can use redux for other globally shared app state (f.e. filtering options).
Use <FlatList /> (react-native), search for sth more suitable?
UPDATE
Some code was changed in mean time (repo), at this time (08.09) one issue still exist and probably causing memory leaks.
You're calling this.socket.on on each componentDidUpdate call (wrongly coded conditions) - continuously adding a new handler!
componentDidUpdate() {
// call all ONLY ONCE afer initial data loading
if (!this.state.updateCoinData && !this.props.cryptoLoaded.length) {
this.setState({updateCoinData: true}) // block condition
this.socket.on('trades', (tradeMsg) => {
// slice() is faster, new array instance
// let updateCoinData = [...this.props.cryptoLoaded];
let updateCoinData = this.props.cryptoLoaded.slice();
for (let i=0; i<updateCoinData.length; i++) {
//Search for changed Crypto Value
if (updateCoinData[i]["short"] == tradeMsg.coin ) {
// found, updating from message
updateCoinData[i]["long"] = tradeMsg["message"]["msg"]["long"]
updateCoinData[i]["short"] = tradeMsg["message"]["msg"]["short"]
updateCoinData[i]["perc"] = tradeMsg["message"]["msg"]["perc"]
updateCoinData[i]["mktcap"] = tradeMsg['message']['msg']["mktcap"]
updateCoinData[i]["price"] = tradeMsg['message']['msg']['price']
//Update the crypto Value state in Redux
this.props.updateCrypto(updateCoinData);
// record found and updated, no more looping needed
break;
}
}
})
}
}
Minor errors: initial fetching states set to true in reducers.
Searching for performance issues I would look at <CoinCard />:
make it PureComponent;
increased and decreased aren't required to be saved at state which forces unnecessasry render calls;
I would use update time (not saved in state, just passed as prop in parent and only for updated rows, within updateCoinData in code above) and derive direction (check for 0 and sign only) of difference (already calculated in perc) only for visible items (from render) and only during time limit (difference between render time and data update prop). setTimeout can be used, too.
finally removing componentWillReceiveProps, componentDidUpdate and shouldComponentUpdate should (highly?) improve performance;

Like Bhojendra Rauniyar said, you should use shouldComponentUpdate in CoinCard. You probably also want to change your FlatList, your downsized sample has the FlatList in a ScrollView, this causes the FlatList to fully expand, thus rendering all it's items at once.
class cryptoTicker extends PureComponent {
componentDidMount() {
this.socket = openSocket('https://coincap.io');
this.props.fetchCoin()
this.props.CurrencyRate()
this.socket.on('trades', (tradeMsg) => {
for (let i=0; i< this.updateCoinData.length; i++) {
if (this.updateCoinData[i]["short"] == tradeMsg.coin ) {
//Search for changed Crypto Value
this.updateCoinData["short"] = tradeMsg["message"]["msg"]["short"]
this.updateCoinData[i]["perc"] = tradeMsg["message"]["msg"]["perc"]
this.updateCoinData[i]["price"] = tradeMsg["message"]['msg']['price']
//Update the crypto Value state in Redux
this.props.updateCrypto(this.updateCoinData);
}
}
})
}
componentWillReceiveProps(newProps){
// Fill with redux data once
if (this.updateCoinData.length < 1 && newProps.cryptoLoaded) {
this.updateCoinData = [...newProps.cryptoLoaded];
}
}
render() {
return (
<View style={{height: '100%'}}>
<Header/>
<FlatList
style={{flex:1}}
data={this.props.cryptoLoaded}
keyExtractor={item => item.short}
initialNumToRender={50}
windowSize={21}
removeClippedSubviews={true}
renderItem={({item, index}) => (
<CoinCard
index={index}
{...item}
/>
)}
/>
</View>
)
}
}
class CoinCard extends Component {
shouldComponentUpdate(nextProps) {
return this.props.price !== nextProps.price || this.props.perc !== nextProps.perc
}
render() {
console.log("here: " + this.props.index);
return (
<View>
<Text> {this.props.index} = {this.props.long} </Text>
</View>
)
}
}

When rendering Flatlist you should consider using PureComponent or utilizing shouldComponentUpdate hook to update only if required.
From the doc:
If your application renders long lists of data (hundreds or thousands of rows), we recommended using a technique known as “windowing”. This technique only renders a small subset of your rows at any given time, and can dramatically reduce the time it takes to re-render the components as well as the number of DOM nodes created.
Take a deep drive to this performance guide.
If you still want some advanced look through, then I will recommend you to look into the following threads:
FlatList and VirtualizedList Scroll performance is laggy after 30+ rows
Performance problems with react when using a big list

You should never do API calls in React's componentWillMount() lifecycle method, instead it should be done in componentDidMount().
Check out this very neat article on lifecycle methods and what should be done in which method: https://medium.com/#baphemot/understanding-reactjs-component-life-cycle-823a640b3e8d.
Many will be tempted to use this function in order to send a request to fetch data and expect the data to be available before the initial render is ready. This is not the case — while the request will be initialized before the render, it will not be able to finish before the render is called.
Instead of creating a socket to update the coinData, you might want to make use of redux subscibe/unsubscribe methods.

Related

How to design my React project better without using shouldComponentUpdate

I am trying to construct 1-minute candlestick.
I have a component that will continuously passing a number (the trade price) to his child component.
This child component will keep update its state: (High, Low, Open, Close) base on the new number he gets from the parent. (e.g. if the number coming in, is higher than the current this.state.high, it will update this.state.high to the new number) After every minute a setInterval function it will take the states and construct a candle and pass it down to its own children.
the state are:
high, low, open, close, newCandle
I got it working by using
shouldComponentUpdate(nextProps:props, nextState:state){
if(this.props !== nextProps)
this.updateStates(nextProps.newTradePrice); //will update the high, low, open, close state
if(JSON.stringify(nextState.nextMinuteCandle) !== JSON.stringify(this.state.nextMinuteCandle) ) //once the one minute interval is up, there will be a function that will auto set the newCandle state to a new Candle base on the current high, low, open, close state
return true;
return false;
}
I read in the document that shouldComponentUpdate should only be used for optimization not to prevent something to reRender. I am using this to prevent reRender and infinite loop.
I've been stuck on this for days, I cant figure out a way to design this better. Any advice on how to design this better?
2nd related question:
In fact I am relying on shouldComponentUpdate for almost ALL my component too. This can't be right. e.g.
I have a CalculateAverageVolume child component, that takes in the this.state.newCandle. And update the volume every time the newCandle changes (i.e. every minute)
constructor(props: props) {
super(props);
this.state = {
[...],
currentAverage: 0,
showVolume: true
};
}
onCloseHandler()
{
this.setState({showVolume: false});
}
updateAvg(newCandleStick: CandleStick){
//do caluation and use this.setState to update the this.state.currentAverage
}
shouldComponentUpdate(nextProps:props, nextState:state)
{
if(JSON.stringify(this.props.candleStick) !== JSON.stringify(nextProps.candleStick) || this.state.showVolume !== nextState.showVolume){
this.updateAvg(nextProps.candleStick);
return true;
}
return false;
}
render() {
return (
<>
{(this.state.showVolume &&
<IndicatorCard
cardHeader="Volume"
currentInfo={this.state.currentAverage.toString()}
onCloseHandler={()=>this.onCloseHandler()}>
</IndicatorCard>
)}
</>
);
}
}
Can someone please teach me how to design this or restructure this? This works perfectly, but doesn't seem like the right way to do it
I would simplify the component like below.
import { useMemo, useState, memo, useCallback } from "react";
function Component({ candleStick }) {
// use props here to calculate average
const updateAverage = () => 0; // use candleStick props to calculate avg here
const [showVolume, setShowVolume] = useState();
// Compute the average from prop when component re-renders
// I would also add useMemo if `updateAverage` is an expensive function
// so that when prop remains same and `showVolume` changes we don't need to calculate it again
const currentAverage = useMemo(updateAverage, [candleStick]);
const onCloseHandler = useCallback(() => setShowVolume(val => !val), []);
return showVolume ? (
<IndicatorCard
cardHeader="Volume"
currentInfo={currentAverage}
onCloseHandler={onCloseHandler}
/>
) : null;
}
// If true is returned, component won't re-render.
// Btw React.memo by default would do shallow comparison
// But if deep comparison function is required, I would use lodash or other utility to do the check instead of JSON.stringify.
const arePropsEqual = (prev, next) =>
isEqual(prev.candleStick, next.candleStick);
export default memo(Component, arePropsEqual);
shouldComponentUpdate is usually reserved for discrete events that you can control. Howevr, it seems like you are dealing with a continuous stream of data.
Two ways to handle it:
Pass down a function reference that handles a stream to the child component and let that handle you state updates in your child component.
Use the context API to inform child component about the changes
Reference implementation :
Upadting State with Context API : https://javascript.plainenglish.io/react-context-api-part-2-updating-state-through-a-consumer-7be723b54d7b
Streams : https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
React with Streams : https://blog.bitsrc.io/how-to-render-streams-with-react-8986ad32fffa
I hit the same spot you're in when starting React. The problem here is that React, at least the basic aspects of it, isn't enough when you're talking about data flow. What you need to look into is a React data management framework, of which Redux is probably the most popular. Go look at Redux and make sure you're looking at the latest documentation based around hooks.
You'll say to yourself "Oh! That makes perfect sense" - I know I did.
Other, similar frameworks are React Query and React's own Context API. The main point I'm trying to make is that you really need data management to do the thing you're looking for.

Mutable global state causing issues with array length

I've been working on a SPA for a while and managing my global state with a custom context API, but it's been causing headaches with undesired rerenders down the tree so I thought I'd give react-easy-state a try. So far it's been great, but I'm starting to run into some issues which I assume has to do with the mutability of the global state, something which was easily solved with the custom context api implementation using a lib like immer.
Here's a simplified version of the issue I'm running into: I have a global state for managing orders. The order object primaryOrder has an array of addons into which additional items are added to the order - the list of available addons is stored in a separate store that is responsible for fetching the list from my API. The orderStore looks something like this:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
}
})
When a user selects to increases the quantity of an addon item, it's added to the addons array if it isn't already present, and if it is the qty prop of the addon is increased. The same logic applies when the quantity is reduced, except if it reaches 0 then the addon is removed from the array. This is done using the following methods on the orderStore:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
},
get orderAddons() {
return orderStore.primaryOrder.addons;
},
increaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
let updatedItem = {
...item,
qty: 1,
};
orderStore.primaryOrder.addons = [
...orderStore.primaryOrder.addons,
updatedItem,
];
} else {
orderStore.primaryOrder.addons[index].qty += 1;
}
console.log(orderStore.primaryOrder.addons);
},
decreaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
return;
} else {
// remove the item from the array if value goes 1->0
if (orderStore.primaryOrder.addons[index].qty === 1) {
console.log("removing item from array");
orderStore.primaryOrder.addons = _remove(
orderStore.primaryOrder.addons,
(i) => i.id !== item.id
);
console.log(orderStore.primaryOrder.addons);
return;
}
orderStore.primaryOrder.addons[index].qty -= 1;
}
}
})
The issue I'm running into has to do with the fact that one of my views consuming the orderStore.addons. My Product component is the consumer in this case:
const Product = (item) => {
const [qty, setQty] = useState(0);
const { id, label, thumbnailUrl, unitCost } = item;
autoEffect(() => {
if (orderStore.orderAddons.length === 0) {
setQty(0);
return;
}
console.log({ addons: orderStore.orderAddons });
let index = orderStore.orderAddons.findIndex((addon) => addon.id === id);
console.log({ index });
if (index !== -1) setQty(orderStore.findAddon(index).qty);
});
const Adder = () => {
return (
<div
className="flex"
style={{ flexDirection: "row", justifyContent: "space-between" }}
>
<div onClick={() => orderStore.decreaseAddonItemQty(item)}>-</div>
<div>{qty}</div>
<div onClick={() => orderStore.increaseAddonItemQty(item)}>+</div>
</div>
);
}
return (
<div>
<div>{label} {unitCost}</div>
<Adder />
</div>
)
}
export default view(Product)
The issue occurs when I call decreaseAddonItemQty and the item is removed from the addons array. The error is thrown in the Product component, stating that Uncaught TypeError: Cannot read property 'id' of undefined due to the fact that the array length reads as 2, despite the fact that the item has been removed ( see image below)
My assumption is that the consumer Product is reading the global store before it's completed updating, though of course I could be wrong.
What is the correct approach to use with react-easy-state to avoid this problem?
Seems like you found an auto batching bug. Just wrap your erroneous mutating code in batch until it is fixed to make it work correctly.
import { batch, store } from '#risingstack/react-easy-state'
const orderStore = store({
decreaseAddonItemQty(item) {
batch(() => {
// put your code here ...
})
}
})
Read the "Reactive renders are batched. Multiple synchronous store mutations won't result in multiple re-renders of the same component." section of the repo readme for more info about batching.
And some insight:
React updates are synchronous (as opposed to Angular and Vue) and Easy State (and all other state managers) use React setState behind the scenes to trigger re-renders. This means they are all synchronous too.
setState usually applies a big update at once while Easy State calls a dummy setState whenever you mutate a store property. This means Easy State would unnecessarily re-render way too often. To prevent this we have a batch method that blocks re-rendering until the whole contained code block is executed. This batch is automatically applied to most task sources so you don't have to worry about it, but if you call some mutating code from some exotic task source it won't be batched automatically.
We don't speak about batch a lot because it will (finally) become obsolete once Concurrent React is released. In the meantime, we are adding auto batching to as many places as possible. In the next update (in a few days) store methods will get auto batching, which will solve your issue.
You may wonder how could the absence of batching mess things up so badly. Older transparent reactivity systems (like MobX 4) would simply render the component a few times unnecessarily but they would work fine. This is because they use getters and setters to intercept get and set operations. Easy State (and MobX 5) however use Proxies which 'see a lot more'. In your case part of your browser's array.splice implementation is implemented in JS and Proxies intercept get/set operations inside array.splice. Probably array.splice is doing an array[2] = undefined before running array.length = 2 (this is just pseudo code of course). Without batching this results in exactly what you see.
I hope this helps and solves your issue until it is fixed (:
Edit: in the short term we plan to add a strict mode which will throw when store data is mutated outside store methods. This - combined with auto store method batching - will be the most complete solution to this issue until Concurrent React arrives.
Edit2: I would love to know why this was not properly batched by the auto-batch logic to cover this case with some tests. Is you repo public by any chance?

React Native elements that are generated outside the Render function does not catch state changes

I have encountered a strange behavior such that when I generate a view outside the Render function, even though it is rendered at the first generation (with the help of this.forceUpdate), it is not re-rendered when the state changes. Which means this does not work properly:
generateTestView() {
let l_generatedView = (
<View
ref={ref => {
this.testAnimatedView = ref;
}}
style={styles.viewOutRender}>
<Text>{'Generated Out of Render - ' + this.state.controlText}</Text>
</View>
);
this.setState({
generatedView: l_generatedView,
buttonToShow: 'showChange',
});
this.forceUpdate();
}
changeControlText() {
this.setState({
controlText: 'NEW VALUE',
});
// this.forceUpdate() also fails here
}
render() {
let l_inRenderView = (
<View style={styles.viewInRender}>
<Text>{'Generated Inside Render - ' + this.state.controlText}</Text>
</View>
);
return (
<View>
... {this.state.generatedView ? this.state.generatedView : undefined}
</View>
);
}
To cross-check, when I generate a view inside the Render function, that view is re-rendered when the state is changed. Which means this works properly as expected:
changeControlText() {
this.setState({
controlText: 'NEW VALUE',
});
}
render() {
let l_inRenderView = (
<View style={styles.viewInRender}>
<Text>{'Generated Inside Render - ' + this.state.controlText}</Text>
</View>
);
return (
<View>
...
{l_inRenderView}
...
</View>
);
}
Related expo snack that can be tested is here. When you click the button "Generate View", it generates a new view and shown properly. Then when you click "Change Control Text", even though the view (generated in Render function, green background) is re-rendered, the other view (generated outside of Render function, yellow background) is not re-rendered.
Please note that, this is a simplified version of a bulk code that I have which is used for animations. Because of some animation related necessities, I need to generate the views outside of the render function.
So the question is how I can force React-Native to re-render the elements that are generated outside of Render function.
Edit: In the actual code there will be hundreds of objects that need to find their locations dynamically and will have discrete animations. That is why regenerating same objects again and again will cause performance problems. That is why I am trying to re-use already generated objects. And that is the reason I am trying to avoid the Render function. Otherwise garbage collector will be in deep trouble.
What you have done here is passed this view fragment
<View
ref={ref => {
this.testAnimatedView = ref;
}}
style={styles.viewOutRender}>
<Text>{'Generated Out of Render - ' + this.state.controlText}</Text>
</View>
to the render function using the state variable. i.e. this.state.generatedView
When you use this syntax, {this.state.generatedView ? this.state.generatedView : undefined}, you are simply embedding that view portion into the greater render() function. However, the embedded view itself is of the same structure, there is no difference from the last embedded view passed to it. Except this variable,
this.state.controlText
If you make no changes to this variable, the internal renderer of react sees the view tree as being identical to the previous tree, and the reconciliation process decides that there are no updates to be made to the view/user interface. It doesn't matter whether the view components are generated in or outside the render function, all that matters is the content returned from the render() function. (into the react framework renderer)
Excerpt from the forceUpdate() docs
React will still only update the DOM if the markup changes.
But yours's hasn't. In order to see a difference,
this.setState({
controlText: 'NEW VALUE' + this.state.controlText,
});
Set your control text to a different value than the previous one. And since you have added the complexity of having part of your view inside the state variable (for reasons that are beyond my comprehension and against every best practice philosophy), call the method that updates the bit of view in your state again.
this.setState({
controlText: 'NEW VALUE' + this.state.controlText,
}, () => generateTestView())
by passing the method as a parameter to the setState function which ensures that the method is only called when the state has updated.
It seems the thing that I was looking for was setNativeProps. Changing the two functions as below solved the problem. I am posting the answer here for anybody who are stuck with similar cases.
generateTestView() {
let l_generatedView = (
<View
ref={ref => {
this.testAnimatedView = ref;
}}
style={styles.viewOutRender}>
<TextInput editable={false} ref={ref => (this.testTextRef = ref)} value={'Generated Out of Render - ' + this.state.controlText} />
</View>
);
this.setState({
generatedView: l_generatedView,
buttonToShow: 'showChange',
});
this.forceUpdate();
}
changeControlText() {
this.setState(
{
controlText: 'NEW VALUE',
},
() => {
this.testTextRef.setNativeProps({text: `Modified in changeControlText - ${this.state.controlText}`});
this.forceUpdate();
}
);
}
Working modified expo snack:
https://snack.expo.io/#mehmetkaplan/3e5325
Credits:
https://medium.com/#payalmaniyar/deep-understanding-of-ref-direct-manipulation-in-react-native-e89726ddb78e
https://hackernoon.com/refs-in-react-all-you-need-to-know-fb9c9e2aeb81

Handling Arrays & Objects in componentDidMount (React)

Imagine that you have a component that bridges an imperative API to react component, for example, Mapbox API.
So you made yourself a Map component and you want the every time that user changes the center prop, the map updates to the new coordinates. It looks something like this:
class Map extends Component {
el = React.createRef();
componentDidMount(props) {
this.map = new mapboxgl.Map({
container: this.el.current,
style: 'mapbox://styles/mapbox/streets-v9',
center: this.props.center,
zoom: this.props.zoom
});
}
componentDidUpdate(prevProps) {
// notice this condition
if (prevProps.center !== this.props.center) {
this.map.setCenter(this.props.center);
}
}
componentWillUnmount() {
this.map && this.map.remove();
}
render() {
return (
<div ref={this.el} className="map" />
)
}
}
And we use it like so:
<Map center={[35.173906, 32.706769]} zoom={16} />
Now here is the catch: since we pass a new array every time, the condition is always true, even for changes unrelated to the map at all.
I made a simple example to demonstrate it:
https://codesandbox.io/s/pmz40wkm8j
Notice that when we increment the counter, the map center updates too.
There are 2 ways I can think of to solve it:
using _.isEqual which will work. but I want if it will affect performance for large arrays and Objects, for example, you can pass a pretty lengthy object of layers to mapbox.
Enforce the user to use Immutable.js, but it doesn't feel right because not everyone feels comfortable with it
What will be the best approach to handle that in your opinion?
I think _.isEqual is superfluous here, just use this function in your componentDidUpdate
centerPropsHasBeenChanged(prevProps, props) {
return (
props.center[0] !== prevProps.center[0] || props.center[1] !== prevProps.center[1]
)
}
Option 2
If you're using big arrays (so not coordinates, but something else), it looks like the fastest option is to JSON.stringify() and compare then

How do I integrate rxjs observables with a plain React Component?

I am new to Rxjs and am trying to learn how to integrate it with a simple React component without any external wrapper/library. I got this working here:
const counter = new Subject()
class App extends Component {
state = {
number: 0
}
componentDidMount() {
counter.subscribe(
val => {
this.setState({ number: this.state.number + val })
}
)
}
increment = () => {
counter.next(+1)
}
decrement = () => {
counter.next(-1)
}
render() {
return (
<div style={styles}>
Current number {this.state.number}
<br /> <br />
<button onClick={this.increment}>Plus</button>
<button onClick={this.decrement}>Minus</button>
</div>
)
}
https://codesandbox.io/s/02j7qm2xw
I trouble is that this uses Subjects which is a known anti-pattern according to experts like Ben Lesh:
https://medium.com/#benlesh/on-the-subject-of-subjects-in-rxjs-2b08b7198b93
I tried doing this:
var counter = Observable.create(function (observer) {
// Yield a single value and complete
observer.next(0);
// Any cleanup logic might go here
return function () {
console.log('disposed');
}
});
class App extends Component {
state = {
number: 0
}
componentDidMount() {
counter.subscribe(
val => {
this.setState({ number: this.state.number + val })
}
)
}
increment = () => {
counter.next(+1)
}
decrement = () => {
counter.next(-1)
}
// - render
}
But this fails with the error: counter.next is not a function
So How would I use new Observable() or Observable.create()and use it to setState with a plain React component?
Because .next() is an Observer's method, NOT Observables.
The reason why Subject works simply because Subject itself is both an observer and an observable. When you call subject.next(), you are simply just updating the observable part, and notify all the observers about the change.
It can be quite confusing sometimes when comes to Observable and Observers. To make it simple, think of this way: Observable is someone who produces the data, a.k.a. data producers; while Observer is someone who consume the data, a.k.a. data consumer. In a simple analogy, consumer eats what is produced. For the same token, Observer(consumer) observes(eats) the observable (produced).
In your context (or at least React/Redux paradigm), Subject works better. That is because Subject has state. It keep tracks of the value over the production of data (job of the Observable). Every time the observable (the one inside Subject) changes, or update, any observers that subscribes to the Subject will get notified. See the pattern similar to redux here? Every time your redux store is updated, your view gets notified (and hence updated). In fact, if you are very used to reactive programming, you can eliminate the use of redux store completely, and fully replace them by Subjects and/or BehaviourSubjects.
For the post from Ben Lesh, he is merely stating this: Always use an Observable if possible, only use Subject when it is really needed. In that particular post, he is stating that a click event can just be an Observable; using Subject will be inappropriate. However, in your context, which is react/redux, using Subject is fine - because the Subject is used to keep track of the state of the store, and NOT the click event handler.
TLDR:
Use Subject if you want to keep track of a state of a variable
.next() is Observer's method, not Observable.

Resources