How can I append react-native components to view without re-rendering? - javascript

Initially componentWillMount() will run a fetch() to an API endpoint & then save the javascript object to the redux store.
Now my problem is that when it comes to rendering the next set of components it re-renders all of them (meaning there's a little flash on the screen because of the rendering).
So essentially onScroll past a certain point it will run the same fetch() api call & grab a new list of javascript objects. It then grabs the data from the redux store & loops through it appending each new postComponent to the layout state.
handleScroll (event: Object) {
const offset = event.nativeEvent.contentOffset.y;
const screenHeight = Dimensions.get('window').height;
// Just for dev purposes until I find a proper way of determining half way down screen
if(offset >= screenHeight/2){
console.log("Halfway past...");
this.props.FeedActions.fetchFeed(this.props.feed.nextUrl, true);
}
}
render() {
var feed = this.props.feed;
if (!_.has(feed, 'posts')) {
return <ActivityIndicatorIOS />;
}
// Append more posts to state
for (var i = 0; i < _.size(feed.posts); i++) {
this.state.postComponents.push(
<PostComponent post={ feed.posts[i] } key={ "post_"+feed.posts[i].postId+Math.random() }/>
);
}
return (
<ScrollView key={Math.random()} onScroll={this.handleScroll.bind(this)}>
{ this.state.postComponents }
</ScrollView>
)
}
};
Is there a way around this? I thought react wouldn't re-render components that are already render, only the ones that are changed? But I guess in this case my components are all dynamic so that means they will be re-rendered.

The problem is in how you're creating your keys. What you want is a key that uniquely identifies that particular node, consistently, and doesn't change every render. Since you use Math.random() as part of your key, it changes the key every render, so react rebuilds that node. Try using postId without the random number trailing it.

Related

Setting state causes a UI lag in React

I have a react app that renders a list of cards in a container widget. The container widget has a useEffect where we subscribe to an observable and update the array that is then used to render the cards inside the component. Each time any of the cards change, the observable emits new values resulting in creating of the array all over again and thus all the cards are re-rendered. However this re-render causes a noticeable UI lag.
Here is the stripped down version of code from the container component.
const obsRef = useRef<Subscription>(null);
const [apiResArray, setApiResArray] = useState([]);
useEffect(() => {
if(obsRef.current) obsRef.current.unsubscribe();
const mySubscription = myObservable$(changingValue)
.subscribe((val) => {
setApiResArray(val.apiArray);
});
obsRef.current = mySubscription;
return () => {
obsRef.current && obsRef.current.unsubscribe();
};
}, [changingValue])
return (
<CardWrapper $showMenu={showMenu}>
{ apiResArray.map((res, resIndex) => {
return (
<Card
data={{
// a json object with props
}}
key={res?.hKey}
/>
);
})}
</CardWrapper>
);
The Card component here simply renders the content based on props passed to it. I know that since data is an Object, referential equality may fail and I have tried memoizing the component but even that does not help.
There is a lot more to these components but posting all the code won't make sense. I wish to understand what possibly might be causing the list re-render to be such a heavy operation that the whole UI gets stuck for a second or two. The array contains around only 100 objects or so. It happens whenever changingValue changes. I can share more information as required.
Any suggestions on improving the performance are highly appreciated.
React will mount and unmount the component in DEV mode to validate effect cleaner and any side effect.
I'm concerned about that subscribe and unsubscribe in your effect.
If you want changingValue to be defined why not just wrap it inside an if statement ?
if (changingValue) {
const mySubscription = myObservable$(changingValue)
.subscribe((val) => {
setApiResArray(val.apiArray);
});
obsRef.current = mySubscription;
}
Another observation is at render of items
<Card
data={
{
// a json object with props
}
}
key={res?.hKey} // Why is this a falsy value ?
/>
Falsy value can make key prop change those re-rendering the component, one with React internal key value and another with your implementation value

Infinite scroll using redux and react

I am trying to implemet infinite scroll in a react based application that uses Redux for state management I am trying to dispatch an action on page scroll. But not able to achieve the same. My code is here
// The Reducer Function
const pinterestReducer = function (state=[], action) {
if (action.type === 'SET_CONTENT') {
state = action.payload;
}
return state;
}
const appReducer = combineReducers({
pinterest: pinterestReducer
})
// Create a store by passing in the pinterestReducer
var store = createStore(appReducer);
window.onscroll = function(ev) {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
// you're at the bottom of the page, show all data
store.dispatch({
type: 'SET_CONTENT',
payload: travelJSON
});
}
};
travelJSON is an array of objects. Initialy I dispatch an action that assigns the first 12 objects of travelJSON to state. When user scrolls to bottom of page I have to assign full JSON Below is my component making use of this state:
// Dispatch our first action to change the state
store.dispatch({
type: 'SET_CONTENT',
payload: travelJSON.slice(0,12)
});
render(
<Provider store={store}><Pinterest /></Provider>,
document.getElementById('page-container'));
I would question why you are trying to do this in the model/business logic layer. Typically virtualizing a scroll is a view state concern. You give the view component the entire list of model objects, but it only renders the DOM elements for the objects that would be shown in the viewport of the view component.
One way to do this is to create a component that allocates a div which is tall enough to display every single one of your model objects. The render method renders only those items that would be displayed in the viewport.
There are a number of components that do this for you already. See for example: https://github.com/developerdizzle/react-virtual-list. This is implemented as an HOC, so you could implement it with your current view logic with minimal changes.
It wraps your component. You send your entire data array into the wrapper, and it figures out which elements will be displayed in the viewport and passes those to the wrapped component, it also passes in the 'paddingTop' style required to shift those elements into the viewport considering the current scroll position.
The following code removes the first 12 items from travelJSON
travelJSON.splice(0,12)
on scroll you replace the first 12 items with the remaining items, while you should really add them instead of replace them.
state = action.payload
To add items to your state use something like this:
return [...state, ...action.payload];
(This uses the new spread operator: https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Operators/Spread_operator )
Have you looked at react waypoint? It seems like a nice component to accomplish exactly what you are trying to do?

KeyboardAvoidingView - Reset height when Keyboard is hidden

I'm using React Natives KeyboardAvoidingView to set the height of my View when the Keyboard is shown. But when I close the the keyboard in the app, the height of the View is not changed back to it's original value.
<KeyboardAvoidingView behavior="height" style={styles.step}>
<View style={styles.stepHeader}>
// my content
</View>
</KeyboardAvoidingView>
The View with the red outline did take up the whole space before I opened and closed the keyboard.
A more detailed explanation on Nisarg's answer.
Create a key for the KeyboardAvoidingView in the constructor
constructor(props) {
this.state = {
keyboardAvoidingViewKey: 'keyboardAvoidingViewKey',
}
}
add listener on the keyboard's will/did hide (and remove it in the willUnmount)
import { KeyboardAvoidingView, Keyboard, Platform } from 'react-native'
componentDidMount() {
// using keyboardWillHide is better but it does not work for android
this.keyboardHideListener = Keyboard.addListener(Platform.OS === 'android' ? 'keyboardDidHide': 'keyboardWillHide', this.keyboardHideListener.bind(this));
}
componentWillUnmount() {
this.keyboardHideListener.remove()
}
update the keyboardAvoidingViewKey in the keyboardHideListener function, should be a new value each time (I used a timestamp) and use this key when rendering the KeyboardAvoidingView element.
keyboardHideListener() {
this.setState({
keyboardAvoidingViewKey:'keyboardAvoidingViewKey' + new Date().getTime()
});
}
render() {
let { keyboardAvoidingViewKey } = this.state
return (
<KeyboardAvoidingView behavior={'height'} key={keyboardAvoidingViewKey} style={...}>
...
</KeyboardAvoidingView>
)
}
Note:
Keep in mind that this will recreate the elements inside the KeyboardAvoidingView (i.e: will call their constructor function, I'm not quite sure why, I'll update the answer after deeper investigation), so you'll have to keep track of any state/prop values that might be overwritten
Update
After a much deeper investigation, I now know why the views are recreated once you change the key.
In order to truly understand why it happens, one must be familiar with how react-native dispatches the render commands to the native side, this particular explanation is pretty long, if it interests you, you can read my answer here. In short, react-native uses Reactjs to diff the changes that should be rendered, these diffs are then sent as commands to a component named UIManager, which sends imperative commands that translate into a layout tree, which changes the layout based on the diff commands.
Once you set a key on a component, reactjs uses this key to identify changes to said component, if this key changes, reactjs identifies the component as a completely new one, which in return sends the initial command to create said component, making all it's children to be created from scratch because there are identified as new elements in a new layout tree, deleting the old tree and creating a new one instead of just adjusting the diffs
If you would like, you can actually spy on these dispatched messages by adding the following code to your App.js file:
import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'
const spyFunction = (msg) => {
console.log(msg);
};
MessageQueue.spy(spyFunction);
If you do that, you'll notice in the logs that each time the key changes, the command that is dispatched in return is createViews, which like stated above creates all the elements that are nested under said component.
Please give key to KeyboardAvoidingView and change when keyboard open and close so it will render and take height
<KeyboardAvoidingView behavior="height" style={styles.step} key={values}>
Wrap components in <KeyboardAvoidingView behavior="padding" style={styles}> on iOS and <View style={styles}> on android
render() {
const ScrollContainer: View | KeyboardAvoidingView =
this.renderDependingOnPlatform();
const scrollContainerParams: any = {};
if (isIOS)
scrollContainerParams.behavior = "padding";
return (
<ScrollContainer style={styles.container} {...scrollContainerParams}>
Scroll and other components
</ScrollContainer>
)}
/**
* Render scroll container depending on platform
* #returns {any}
*/
renderDependingOnPlatform() {
if (isAndroid())
return View;
else return KeyboardAvoidingView;
}
A simple workaround is to set the behavior property of the KeyboardAvoidingView to 'padding'. This avoids the issue of recalling the constructor function, which allows you to safely store information in state (say you have two Inputs and you want to store the value of the text in state even if the user collapses the keyboard in between clicking the two inputs).
This method may slightly alter the layout of the KeyboardAvoidingView's children, so be aware of that.

Fill a container with content using React.JS

I have a container element, which is also a React component, with a specific height. I also have an API that returns blocks of contents of variable length, based on the requested ID.
<Contents/>
I want to request a new block of content from the API until the container is overflowing.
My code works, but it rerenders all content blocks and adds one in each render, until the container is full. This seems like a bad approach.
class Contents extends React.Component {
constructor() {
super();
this.state = {
numElements: 0
};
}
render() {
const elements = [];
for(let i = 0; i < this.state.numElements; i++) {
elements.push(this._getElementContents(i));
}
return(<div id="contents">
{elements.map(element => element)}
</div>);
}
componentDidMount() {
// start the 'filling loop'
this._addElement();
}
componentDidUpdate() {
// keep adding stuff until container is full
if(document.getElementById('contents').clientHeight < window.outerHeight - 400) {
this._addElement();
}
}
_addElement() {
// setState will cause render() to be called again
this.setState({numElements: this.state.numElements + 1});
}
_getElementContents(i) {
// simplified, gets stuff from API:
let contents = api_response;
return(<Element key={i} body={contents} />);
}
}
How can I append elements to the container until it is filled, without re-adding, re-querying the API and re-rendering existing elements on each loop?
I can't see how you are calling your API or under what condition. From my understanding you should do two things.
Keep elements array into the state object and push new elements whenever they arrive.
Use the shouldComponentUpdate instead of componentDidUpdate with the same exactly condition to judge when you have to request more elements from your API.
Eventually draw the state.elements. Whenever you receive a new one you just use the local elements you previously got to redraw the component instead of making all API calls all over again
Because this.setState() triggers a re-render, like dimitrisk said, you should put logic in shouldComponentUpdate to only render once you have everything ready to go to the DOM.
You should look into Dynamic Children in React. It helps React do reconciliation when re-rendering http://facebook.github.io/react/docs/multiple-components.html#dynamic-children
http://revelry.co/dynamic-child-components-with-react/

Updating state in react-native taking forever

I added the ability to 'like' a post.
And suddenly react-native is crawling at a snails pace.
Im new to react so aren't really sure where and when to properly set state.
How should I properly attach the ability to like a row, in a listView table??
Heres the code...
The 'like' text to click on is this....
<Text onPress={this.likePost.bind(this, feeditem.post_id)}>{feeditem.likes} likes</Text>
Then the 'liking' function is this...
likePost(id){
if(this.state.feedData!=null){
var d = this.state.feedData;
// Find row by post_id
var ix = d.findIndex(x => x.post_id===id);
// Only set state if user hasn't already liked
if(d[ix].you_liked==0){
// Mark as liked, and increment
d[ix].you_liked=true;
d[ix].likes = parseInt(d[ix].likes)++;
// Set the state after liking
this.setState({
feedData: d,
feedList: this.state.feedList.cloneWithRows(d)
});
}
}
}
It works, and it properly updates the data, and shows in the dev tools just fine. However, the rendering of the new state visually is taking over a minute.
Am I updating the state wrong?
What is the cost for setState? I thought react was supposed to just re-render the changes it sees in the virtual DOM. Why then does it seem like its rerendering my entire listView. And why over 1 minute load time??
(obvious memory leak somewhere)??
Also, is it possible to just increment the integer on the 'DOM' without triggering re-renders?
Render your ListView passing dataSource and renderRow props
render() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this._renderRow}/>
);
}
Create renderRow function, where your can access your data and index of clicked row
_renderRow = (rowData, sectionIndex, rowIndex, highlightRow) => {
return (
<TouchableOpacity onPress={() => {
//Here you shoud update state
}}/>
);
};
Furthermore, you always need to make copy of a state before manipulating it, because its immutable object. Try working with state mutating libraries, use https://facebook.github.io/react/docs/update.html or other options.
Animations can take a long time because of the chrome debugger.
A small way to mitigate this is using InteractionManager
likePost(id){
InteractionManager.runAfterInteractions(() => {
if(this.state.feedData!=null){
var d = this.state.feedData;
// Find row by post_id
var ix = d.findIndex(x => x.post_id===id);
// Only set state if user hasn't already liked
if(d[ix].you_liked==0){
// Mark as liked, and increment
d[ix].you_liked=true;
d[ix].likes = parseInt(d[ix].likes)++;
// Set the state after liking
this.setState({
feedData: d,
feedList: this.state.feedList.cloneWithRows(d)
});
}
}
});
}
Or disable chrome debugging and use android monitor to see debugging messages.

Categories

Resources