I want to implement pagination. So when a user scrolls down to the bottom I want to make an api call. I see through window.scroll I can find position of scroll and can achieve that. However I want to access redux state to get certian data. Since this event is not bind by any component I won't be able to pass down data. What would be the correct approach in this scenario?
If I want to access redux store through a simple function How can I do that? And on scroll how do I make sure that only request goes through?
You can connect your component that does the scroll. or you can pass props to the component that have the store information. Those are the two recommended ways to reach your store. That being said you can also look at the context
class MyComponent extends React.Component {
someMethod() {
doSomethingWith(this.context.store);
}
render() {
...
}
}
MyComponent.contextTypes = {
store: React.PropTypes.object.isRequired
};
Note: Context is opt-in; you have to specify contextTypes on the component to get it.
Read up on React's Context doc It may not be a complete solution since it could be deprecated in a future version
Edit:
Per the comments with the clarity you provided you can just do this.
import React, { Component } from 'react';
import ReactDOM = from 'react-dom';
import _ from 'lodash';
const defaultOffset = 300;
var topOfElement = function(element) {
if (!element) {
return 0;
}
return element.offsetTop + topOfElement(element.offsetParent);
};
class InfiniteScroll extends Component {
constructor(props) {
super(props);
this.listener = _.throttle(this.scrollListener, 200).bind(this);
}
componentDidMount() {
this.attachScrollListener();
}
componentWillUnmount() {
this.detachScrollListener();
}
scrollListener () {
var el = ReactDOM.findDOMNode(this);
var offset = this.props.offset || defaultOffset;
var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
if (topOfElement(el) + el.offsetHeight - scrollTop - window.innerHeight < offset) {
this.props.somethingHere;
}
}
attachScrollListener() {
window.addEventListener('scroll', this.listener);
window.addEventListener('resize', this.listener);
this.listener();
}
detachScrollListener() {
window.removeEventListener('scroll', this.listener);
window.removeEventListener('resize', this.listener);
}
render() {
return (...)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(InfiniteScroll);
I added lodash to the import here so you can throttle the scroll listener function. you only want to call the handler function so many times a second or it can start lagging the page (depending on how heavy the listener function is)
The correct way to access your application state in components is the usage of react-redux and selectors functions.
react-redux provides a function which is called connect. You should use this function to define which values from our state you want to map to the props of the component so these will be available.
The function you need for this mapping is called mapStateToPropswhich returns an object with the values which should be passed to the component.
Also you can be define redux actions which should be made available in the component (e.g. for trigger the load of the next page). The function is called mapDispatchToProps.
Here an example:
import React from 'react';
import { connect } from 'react-redux';
import { getUsersPage } from './selectors';
import { loadUsersPage } from './actions';
class MyComponent extends React.Component {
handleScroll () {
this.props.loadUsersPage({ page: lastPage + 1 });
}
render () {
const users = this.props.users;
// ...
}
}
const mapStateToThis = (state) => {
return {
users: getUsers(state)
};
}
const mapDispatchToProps = (dispatch) => {
return {
loadUsersPage: (payload) => {
dispatch (loadUsersPage(payload));
}
}
};
export default connect()(MyComponent);
Related
React is actually unbelievably hard T_T....
I just want to invoke a component method when there's a state change. I can do this easily with a watcher in Vue. But what am I supposed to do in React class component and MobX's autorun? Would this work in a functional component instead?
import someStore
#observer
class MyComponent {
constructor(){
autorun(
// references someStore.someObservable
// call component methods
// but this isn't hit when there's change
)
}
}
I've made 2 examples for you, one with class component which is not recommended way to do things anymore, and one with functional component.
Example is quite escalated, because it would be much easier to compute our status in render, but let's pretend we can't do that and we want to invoke some internal method.
First, we setup out store and update it every second:
import { observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import React, { useEffect, useState } from 'react';
const store = observable({
counter: 0
});
setInterval(() => {
store.counter++;
}, 1000);
// Helper method
const getStatus = (number) => (number % 2 === 0 ? 'even' : 'odd');
Here is our functional component, we use useEffect to react to counter changes and then update our internal state with setStatus method:
const CounterFunctional = observer(() => {
const [status, setStatus] = useState(() => getStatus(store.counter));
useEffect(() => {
setStatus(getStatus(store.counter));
}, [store.counter]);
return <div>functional: {status}</div>;
});
Here is our class component, now we use MobX reaction (don't forget to dispose it on unmount) and similarly update our internal state after counter changes:
const CounterClass = observer(
class extends React.Component {
disposer;
constructor() {
super();
this.state = {
status: getStatus(store.counter)
};
}
componentDidMount() {
this.disposer = reaction(
() => store.counter,
() => {
this.setState({
status: getStatus(store.counter)
});
}
);
}
componentWillUnmount() {
this.disposer();
}
render() {
return <div>class: {this.state.status}</div>;
}
}
);
Hope it makes sense, React is actually super easy library :)
Codesandbox: https://codesandbox.io/s/httpsstackoverflowcomquestions66602050-7uhm6
I have a component which works with third party library and I need to add listener after component mounts. For some reason I can't add listeners without data which asynchronously fetched from server and passed through 'connect' function (from react-redux).
How to prevent React component mounting if props are empty?
After some research I haven't found a solution, so I wrote mine:
// No Props No Mount
import React from 'react';
import _ from 'lodash';
function NPNM(WrappedComponent) {
return class extends React.Component {
render() {
const { children } = this.props;
const data = _.omit(this.props, children);
let hasProps = true;
_.forEach(data, elm => {
if (_.isEmpty(elm) && !_.isFunction(elm)) hasProps = false;
});
return hasProps ? <WrappedComponent {...this.props} /> : <></>;
}
};
}
export default NPNM;
It should be used like
connect(mapStateToProps, mapDispatchToProps)(NPNM(YourComponent));
I have a component which, depending on its prop (listId) listens to a different document in a Firestore database.
However, when I update the component to use a new listId, it still uses the previous listener.
What's the correct way to detach the old listener and start a new one when the component receives new props?
Some code:
import React from 'react';
import PropTypes from 'prop-types';
import { db } from '../api/firebase';
class TodoList extends React.Component {
state = {
todos: [],
};
componentWillMount() {
const { listId } = this.props;
db.collection(`lists/${listId}/todos`).onSnapshot((doc) => {
const todos = [];
doc.forEach((t) => {
todos.push(t.data());
});
this.setState({ todos });
});
};
render() {
const { todos } = this.state;
return (
{todos.map(t => <li>{t.title}</li>)}
);
}
}
TodoList.propTypes = {
listId: PropTypes.object.isRequired,
};
export default TodoList;
I've tried using componentWillUnmount() but the component never actually unmounts, it just receives new props from the parent.
I suspect that I need something like getDerivedStateFromProps(), but I'm not sure how to handle attaching / detaching the listener correctly.
Passing a key prop to the TodoList lets the component behave as it should.
My component is straight forward:
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as instanceActions from '../../../store/instances/instancesActions';
class InstanceDetailsPage extends Component {
componentWillReceiveProps(nextProps) {
console.log('will receive props');
if (nextProps.id !== this.props.id){
this.updateInstanceDetails();
}
}
updateInstanceDetails = () => {
this.props.actions.loadInstance(this.props.instance);
};
render() {
return (
<h1>Instance - {this.props.instance.name}</h1>
);
}
}
function getInstanceById(instances, instanceId) {
const instance = instances.filter(instance => instance.id === instanceId);
if (instance.length) return instance[0];
return null;
}
function mapStateToProps(state, ownProps) {
const instanceId = ownProps.match.params.id;
let instance = {id: '', name: ''};
if (instanceId && state.instances.length > 0) {
instance = getInstanceById(state.instances, instanceId) || instance;
}
return {
instance,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(instanceActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(InstanceDetailsPage);
My reducer which I'm pretty sure I'm not mutating the state:
import * as types from '../actionTypes';
import initialState from '../initialState';
export default function instancesReducer(state = initialState.instances, action) {
switch (action.type){
case types.LOAD_INSTANCES_SUCCESS:
return action.instances.slice(); // I probably don't event need the .slice() here, but just to be sure.
default:
return state;
}
}
I know for sure that the state change which triggers props change because I logged this.props on the render method, and the props changed couple of times!
With that, the componentWillReceiveProps wasn't called even once.
What can cause that?
there are a few reasons why componentWillReceiveProps will not be called,
if its not receiving a new props object from the redux store
additionally these methods are not called if a component is mounted. So you may see your component update, but it is really just mounting and unmounting. to fix this, look into the parent rendering this component and check to see if it is rendering the component with a different key or if there is some sort of conditional render that may return false and cause react to unmount it.
My situation is, I have the Navigation component, which is the base, and is listening to the Navigations state(Redux). It should be extended to HorizontalNavigation and VerticalNavigation, for easy reusable code in the future.
My problem is, right now I already have the "final" version of Navigation.jsx and I can extend it, as a class, but can't override it's methods. It triggers the super(Navigation) method and not the final one. I need to override the methods in Horizontal or Vertical components.
There is no code erros on console, so it isn't something breaking, but that I don't know how to handle how to extend it.
Navigation.jsx
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { itemAction, stageAction } from 'Store/Actions/Actions';
class Navigation extends Component {
// ACTIONS
leftAction () {
this.onLeftAction();
}
onLeftAction () {}
rightAction () {
this.onRightAction();
}
onRightAction () {}
downAction () {
this.onDownAction();
}
onDownAction () {}
upAction () {
this.onUpAction();
}
onUpAction () {}
// STAGES
nextStage (slug) {
this.goToStage(slug);
}
previousStage (slug) {
this.goToStage(slug);
}
goToStage (slug) {
// Just for illustration purpose
// let { dispatch } = this.props;
// dispatch(stageAction(slug));
}
// ITEMS
nextItem (index) {
this.goToItem(index);
}
previousItem (index) {
this.goToItem(index);
}
goToItem (index) {
// Just for illustration purpose
// let { dispatch } = this.props;
// dispatch(itemAction(index));
}
render () {
return ();
}
}
function mapStateToProps (state, props) {
navigation: state.Navigations[props.slug]
}
export default connect(mapStateToProps)(Navigation);
Horizontal.jsx
import React from 'react';
import Navigation from 'Components/Navigation/Navigation';
class HorizontalNavigation extends Navigation {
onLeftAction (previousItemIndex) {
this.previousItem(previousItemIndex);
}
onRightAction (nextItemIndex) {
this.nextItem(nextItemIndex);
}
onTopAction (slug) {
this.previousStage(slug);
}
onDownAction (slug) {
this.nextStage(slug);
}
}
export default HorizontalNavigation;
The VerticalNavigation would be the opposite. Left and right for stage; up and down for items.
I don't want to reuse the Navigation component each time I could use Horizontal or Vertical, and rewrite the same exact logic over and over again.
I'm using the Higher-Order Component pattern, exporting a function to connect the extended component, eg:
import { connect as reduxConnect } from 'react-redux'
...
export class Navigation extends Component{
...
export function connect(Component){
return reduxConnect(
(state, props)=>({...})
)(Component);
}
export default connect(Navigation)
And in the Horizontal.jsx you could do
import { Navigation, connect } from './Navigation';
class Horizontal extends Navigation{
...
export default connect(Horizontal);
This way, you keep the connect(mapStateToProps) in one place.
This is a fun one. At the bottom of Navigation, you're exporting the connecting component, which in essence is exporting the class created in connect, which is not the same class as Navigation. So, when you extend the default exported class, you're actually extending the connect class. That's a mouthful.
To get this to work, you could also export your class (in addition to export default connect(mapStateToProps)(Navigation); at the bottom:
export class Navigation extends Component {
Then to extend it, you can do:
import { Navigation } from './Navigation';
class Horizontal extends Navigation {
// ...
However, you would also need connect the Horizontal component as well in order to get the right props from redux.
If you don't want to use connect, you could take in props to your Navigation component that changes how those up/down/left/right actions work, then you could create a Horizontal/Vertical component that passes in the right props. Something like:
class Horizontal extends React.Component {
constructor(props, context) {
super(props, context);
this.onUp = this.onUp.bind(this);
this.onDown = this.onDown.bind(this);
this.onLeft = this.onLeft.bind(this);
this.onRight = this.onRight.bind(this);
}
onUp() {}
onDown() {}
onLeft() {}
onRight() {}
render() {
return (
<Navigation onUp={this.onUp} onDown={this.onDown} onLeft={this.onLeft} onRight={this.onRight} />
);
}
);