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?
Related
I'm new to React and building a calendar application. While playing around with state to try understand it better, I noticed that my 'remove booking' function required a state update for it to work, while my 'add booking' function worked perfectly without state.
Remove bookings: requires state to work
const [timeslots, setTimeslots] = useState(slots);
const removeBookings = (bookingid) => {
let newSlots = [...timeslots];
delete newSlots[bookingid].bookedWith;
setTimeslots(newSlots);
}
Add bookings: does not require state to work
const addBookings = (slotid, tutorName) => {
timeslots[slotid].bookedWith = tutorName;
}
I think that this is because of how my timeslot components are rendered. Each slot is rendered from an item of an array through .map(), as most tutorials online suggest is the best way to render components from an array.
timeslots.map(slot => {
if (!slot.bookedWith) {
return <EmptyTimeslot [...props / logic] />
} else {
return <BookedTimeslot [...props / logic]/>
}
})
So, with each EmptyTimeslot, the data for a BookedTimeslot is available as well. That's why state is not required for my add bookings function (emptyTimeslot -> bookedTimeslot). However, removing a booking (bookedTimeslot -> emptyTimeslot) requires a rerender of the slots, since the code cannot 'flow upwards'.
There are a lot of slots that have to be rendered each time. My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots? This I assume would require state to be used for both the add booking and remove booking function. Like this:
for (let i=0;i<timeslots.length;i++) {
if (!timeslot[i].bookedWith) {
return <EmptyTimeslot />
} else {
return <BookedTimeslot />
}
}
Hope that makes sense. Thank you for any help.
Your addBooking function is bad. Even if it seems to "work", you should not be mutating your state values. You should be using a state setter function to update them, which is what you are doing in removeBookings.
My question is therefore, instead of mapping each slot (with both and information present in each slot), would it be more efficient to use a for loop to only render the relevant slot, rather than the information for both slots?
Your map approach is not rendering both. For each slot, it uses an if statement to return one component or the other depending on whether the slot is booked. I'm not sure how the for loop you're proposing would even work here. It would just return before the first iteration completed.
This I assume would require state to be used for both the add booking and remove booking function.
You should be using setTimeslots for all timeslot state updates and you should not be mutating your state values. That is true no matter how you render them.
Here is something I don't understand about Redux. I have an app which goes through items. You can go to the previous item and to the next item. As I understand it, you are not supposed to access the current state in your actions.
As regards my app, I have an array in my redux state which holds all the ids of my items: ["3123123123","1231414151","15315415", etc.] and I have a piece of state which holds the currently selected item (or better, it holds the id of that item). Now when the user clicks nextItem, I need to get the next item. My (unfinished) action look like this:
export function nextItem(currentId) {
//my idea:
//take the currentId, look where in the array it is, and get the position
//increment that position
//get the id of the next item in the array (at position: position+1)
//update state
return {
type: SET_CURRENT_ITEM,
payload: item
}
}
Similar things would apply to the previous Item action creator. However, I am at loss how to implement this action creator, without accessing my current state? Where and how would that ideally happen?
I would suggest that you dispatch an action like:
{
type: INCREMENT_CURRENT_ITEM
}
You can dispatch this from within any connected component directly:
dispatch({ type: INCREMENT_CURRENT_ITEM })
Or if you prefer to use an action creator, that's fine too:
dispatch(incrementItem()) // incrementItem() returns the action above
In your reducer, you have access to the current state, which is where you can increment the item index, without having to search for the current value in an array.
I probably would add a component responsible of items id incrementing through the app
import React from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { nextItem } from "../redux/actions";
const ItemNav = ({ nextItem, items, item }) => {
function setNextItem() {
let currentItemID = items.indexOf(item) + 1;
if (currentItemID >= items.length - 1) currentItemID = 0;
nextItem(items[currentItemID]);
}
return (
<ul>
<li>previous item</li>
<li onClick={setNextItem}>next item</li>
</ul>
);
};
const mapStateToProps = state => ({
items: state.items,
item: state.item
});
const mapDispatchToProps = dispatch => bindActionCreators({ nextItem },dispatch);
export default connect(
mapStateToProps,
mapDispatchToProps
)(ItemNav);
The reducer is a pure function. The producer must receive
arguments of the same type, the producer must calculate the new
version of the state and return it. No surprises. No side effects. No
calls to the third-party API. No changes (mutations). Only the
calculation of the new version of the state.
Pure function - At the fundamental level, any function that does not
change the input does not depend on the external state (database, DOM
or global variable) and returns the same result for the same input
data as a pure function.
In addition, if the values are in a different reducer, what to do?
Action creators - also pure functions, and for calculation we must
receive data from the store
Component - use business logic in the component bad practice
Remain middleware and to not produce many middleware it is better to
use redux-thunk
In addition, a link to a similar question:
Redux: Reducer needs state of other Reducer?
And the link to the first found project realizing this situation:
https://github.com/rwieruch/favesound-redux/blob/master/src/actions/player/index.js
I have a function that changes the screen and sets the state at the same time that works given below (initial state of weapon is null):
var { navigate } = this.props.navigation;
clickM16 = () => {
this.setState({
weapon: "M16"
});
navigate("Second", {weapon: this.state.weapon});
}
And I am calling this on my second screen via {this.props.navigation.state.weapon}, but the state doesn't seem to update to M16 until I go back and click the button again.
I have console logged both above and below the setState function and on the first click it always gives me null but M16 when I go back and click it again.
Can I not run setState at the same time as navigating between screens? If I can what am I doing wrong.
TLDR: I'm trying to set state and change page in same function so I can then display the new state on the new page as text. The state change doesn't happen until the second click of the button.
Try putting a small timeout for the navigate. The state change may not be complete when you hit the navigate instruction
var { navigate } = this.props.navigation;
clickM16 = () => {
this.setState({
weapon: "M16"
});
setTimeout(() => navigate("Second", {weapon: this.state.weapon}),20);
}
State is supposed to be used as a helper to handle a small amount of data inside your component. The state life cycle ends as the component it belongs completely unmount. Also, note that setState is an asynchronous function, so you must not rely on React to handle sync situations for you. Updating your state will also make your component rerender, so you should use it carefully to avoid loss memory unnecessarily.
If you just want to pass data from a component to another, in this case using navigation props is enough, like this navigate("Second", {weapon: 'M16'});. You don't need to update your state to then be able to pass this data further. In fact, it makes no sense to update your state before navigation, since the current state itself will be lost in the next screen.
If you need to share the exact same state prop between more components, which doesn't seem to be the case, maybe you should consider using another approach, like Redux (https://redux.js.org/).
I recommend you to read the official docs for more detailed info:
https://reactjs.org/docs/react-component.html#state
https://reactjs.org/docs/state-and-lifecycle.html
Hope it helps
Edit:
Based on the information you provided below, if weapon will be an array, for example, and you need to push a new value to it before navigation, you should not use setState, try this instead:
const { navigate } = this.props.navigation;
clickM16 = () => {
const { weapon } = this.state;
weapon.push('M16');
navigate("Second", { weapon });
}
Hope it helps
I will give another suggestion:
var { navigate } = this.props.navigation;
clickM16 = () => {
this.setState({
weapon: "M16"
});
let sendPara = this.state.weapon
navigate("Second", {weapon: sendPara});
}
Recive parameter in respective component.
catName={this.props.navigation.state.params.sendPara}
I hope this may help you.
I'm pretty new to react and have been working on this new page for work. Basically, there's a panel with filter options which lets you filter objects by color. Everything works but I'm noticing the entire filter panel flickers when you select a filter.
Here are the areas functions in the filter component I think bear directly on the filter and then the parent component they're inserted into. When I had originally written this, the filter component was also calling re render but I've since refactored so that the parent handles all of that - it was causing other problems with the page's pagination functionality. naturally. and I think that's kind of my problem. the whole thing is getting passed in then rerendered. but I have no idea how to fix it. or what's best.
checks whether previous props are different from props coming in from parent and if so, creates copy of new state, calls a render method with those options. at this point, still in child component.
componentDidUpdate(prevProps, prevState) {
if (prevState.selectedColorKeys.length !== this.state.selectedColorKeys.length ||
prevState.hasMatchingInvitation !== this.state.hasMatchingInvitation) {
const options = Object.assign({}, {
hasMatchingInvitation: this.state.hasMatchingInvitation,
selectedColorKeys: this.state.selectedColorKeys
});
this.props.onFilterChange(options);
}
}
handles active classes and blocks user from selecting same filter twice
isColorSelected(color) {
return this.state.selectedColorKeys.indexOf(color) > -1;
}
calls to remove filter with color name so users can deselect with same filter button or if its a new filter, sets state by adding the color to the array of selected color keys
filterByColor(color) {
if (this.isColorSelected(color.color_name)) {
this.removeFilter(color.color_name);
return;
}
this.setState({
selectedColorKeys:
this.state.selectedColorKeys.concat([color.color_name])
});
}
creating the color panel itself
// color panel
colorOptions.map(color => (
colorPicker.push(
(<li className={['color-filter', this.isColorSelected(color.color_name) ? 'active' : null].join(' ')} key={color.key} ><span className={color.key} onClick={() => { this.filterByColor(color); }} /></li>)
)
));
parent component
callback referencing the filter child with the onFilterChange function
<ThemesFilter onFilterChange={this.onFilterChange} />
onFilterChange(filters) {
const { filterThemes, loadThemes, unloadThemes } = this.props;
unloadThemes();
this.setState({
filterOptions: filters,
offset: 0
}, () => {
filterThemes(this.state.filterOptions.selectedColorKeys, this.state.filterOptions.hasMatchingInvitation);
loadThemes(this.state.offset);
});
}
when I place break points, the general flow seems to be :
filterByColor is triggered in event handler passing in that color
active classes are added to the color, a filter tag for that color is generated and appended
componentDidMount takes in the previous props/state and compares it to the new props/state. if they don't match, i.e state has changed, it creates a copy of that object, assigning the new states of what's changed. passes that as props to onFilterChange, a function in the parent, with those options.
onFilterChange takes those options, calls the action method for getting new themes (the filtering actually happens in the backend, all I really ever need to do is update the page) and passes those forward. its also setting offset to 0, that's for the page's pagination functionality.
It looks like the problem might be around the componentDidUpdate function which, after setting breakpoints and watching it go through the steps from filterByColor to componentDidMount, that componentDidMount loops through twice, checking again if the colorIsSelected, and throughout all that the color panel pauses to re-render and you get a flicker.
Is it possible creating the copy is causing it? since it's being treated, essentially, as a new object that isColorSelected feels necessary to double check? any help you guys have would be much appreciated, this shit is so far over my head I can't see the sun.
Can you change
componentDidUpdate(prevProps, prevState)
with
componentWillUpdate(nextProps, nextState)
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.