I'm trying to show loading while data is fetching from API and I want a higher-order component to achieve this. But my code causes an infinite loop
//home.tsx file
export class Home extends Component<Props, any> {
componentDidMount(): void {
this.props.getCharacters();
}
render() {
return (
<div></div>
)
}
}
const mapStateToProps = (state: AppState): StateProps => {
return {
loading:state.marvel.loading,
data: getResultsSelector(state),
};
};
const mapDispatchToProps: DispatchProps = {
getCharacters,
};
export default connect(mapStateToProps, mapDispatchToProps)(WithLoading(Home));
//hoc.tsx file
function WithLoading(Component:any) {
return function WihLoadingComponent({ loading, ...props }:any) {
console.log(loading)
if (!loading) return <Component {...props} />;
return <p>Hold on, fetching data might take some time.</p>;
};
}
export default WithLoading;
How can I fix this issue ?
I assume that getCharacters() is the function that fetches data from the API.
The Component is mounted depending on loading, but loading is set when the Component is mounted.
if (!loading) return <Component />; mounts the component.
componentDidMount invokes getCharacters()
getCharacters() sets loading = true
if (!loading) return <Component />; does not return the Component anymore.
when loading is done: loading = false
if (!loading) return <Component />; mounts the component again.
continue at 2.
The loading and mounting must be made independent of each other.
A. Either you should not invoke the loading inside the Component,
B. or you should not mount the Component conditionally.
A: separate the loading from the Component
As you probably want to create a generic HOC that shows some arbitrary Component only when loaded === true, you might prefer A., which means you have to design the Components so that they do not change loading themselves.
There might be ways to do that, e.g.:
componentDidMount(): void {
if( !this.props.dataHasAlreadyBeFetched ){
this.props.getCharacters();
}
}
but I think that would be bad style, and it seems to me that in your case it makes sense to separate it, because apparently the only reason your Component is mounted the first time is to invoke getCharacters, which unmounts the Component.
B: not un-mount the Component
The Component is always mounted (without if( !loading)), and itself has to be designed to render any content only if loading === true (and otherwise null). Of course, loading has to be passed to the Component as well.
Related
I'm encountering a small issue for which I did fix the problem but I'm really looking for an better solution !
Let's suppose you have a Parent A component which role is to dispatch an action to fetch data.
class ParentA extends Component {
constructor(props) {
super(props)
const { dispatch } = this.props;
dispatch(actionRequest({ clientId: props.match.params.customerId }))
}
render() {
const { customer, isFetching } = this.props;
if(isFetching){
return <Spinner />
}
if(!customer){
return null
}
return <CustomerDetailsPage customerId={this.props.match.params.customerId} customer={customer} {...this.props} />
}
}
export default connect(state => ({
customer: getClient(state),
isFetching: isClientFetching(state)
}))(ParentA)
Nothing very complicated here.
Let's suppose I dispatch an action in my saga before the api call, to set isFetching to true, and one after the api call success or error to isFetching back to false.
Of course my initial state for this reducer has isFetching to false.
My action creators look like that (dont pay attention the wrapper reducer, the important thing is is the different actions )
const setFetching = isFetching => state => state.set('fetching', isFetching)
export default createReducer(initialState, {
[actionSuccess]: [setClient],
[actionRequest]: [setFetching(true)],
[actionFulfill]: [setFetching(false)],
})
The problem, to summarize is this one : when the reducer is at its initial state, there is no problem because I will put the fetched data for the first time so it is null during the first render.
The thing is about when the ParentA component unmounts, redux still store the previous value.
So when I come back to ParentA, the selector in the connect function has already a value.
Its causes at the end a useless first render of the child of ParentA since isFetching is false and customer in my exemple is not null as I just say.
In the end it causes a useless child rendering but imagine the child of ParentA fetched itself data, then it causes 2 fetches from the child !
I solved this by moving the isFetching and customer deeper in the tree, in the child but I would like to avoid splitting my props and handle this in ParentB.
I cannot memoized ParentA because the isFetching is indeed changing.
What would you eventually suggest?
I know this won't be a satisfying answer, but you would have an easier time if you were using functional components with hooks. The useEffect hook is very powerful and would 100% help you solve this problem easily. Otherwise, in Class Components, you will need to leverage lifecycle hooks like ComponentDidMount, ComponentWillUnmount etc.
I'm not sure if this is what you are looking for but these tips might help you down the line.
For your issue it would be better if you stored your data (customer & isFetching) as part of the class's state and make the action return the fetched value.
Try using lifecycle methods to handle/manipulate your state. That is instead of dispatching an action in the constructor you could do the same in componentDidMount/componentDidUpdate methods.
Try extending your class from React.PureComponent instead of React.Component. PureComponent makes your life easier by preventing unnecessary re-renders by comparing to see if the state or the props have changed. Whereas if you extend your class off a component it is up to you to manage the re-renders using the shouldComponentUpdate lifecycle method.
The below structure might help you solve your current issue.
import React, {PureComponent} from 'react'
const DEFAULT_CUSTOMER = {id: ''}
class ParentA extends PureComponent {
constructor(props) {
super(props)
this.state = {
customer: DEFAULT_CUSTOMER,
isFetching: false,
}
}
// Will be called once the component has been mounted successfully
componentDidMount() {
this.fetchCustomer()
}
//Incase your component doesn't un mount and just re-renders the content based on the client id
// you might have to use the componentDidUpdate
// will get invoked everytime the props or the state changes
componentDidUpdate(prevProps, prevState) {
const { clientId: oldClientId } = prevProps
const { clientId } = this.props
// essential for you to do some sort of check to prevent an infinite loop
if ( clientId !== oldClientId ) {
//will reset the customer to the default customer
this.setCustomer()
this.fetchCustomer()
}
}
// Set the value of isFetching
setIsFetching => (isFetching) =
this.setState({
isFetching,
})
// Set the value of customer
setCustomer => (customer = DEFAULT_CUSTOMER) =
this.setState({
customer,
})
// Dispatches the action to fetch customer
fetchCustomer => async () = {
try {
const { match, dispatch } = this.props
const { clientId } = match.params
this.setIsFetching(true)
// Assuming you are passing back the JSONIFIED response back
const resp = await dispatch(actionRequest({ clientId, }))
this.setCustomer(resp.body.customer)
this.setIsFetching(false)
} catch (e) {
this.setIsFetching(false)
}
}
render() {
const { customer, isFetching } = this.state;
if(isFetching){
return <Spinner />
}
if(!customer.id){
return null
}
return <CustomerDetailsPage customerId={this.props.match.params.customerId} customer={customer} {...this.props} />
}
}
export default ParentA
I am trying to implement a shared state into my application using the React context api.
I am creating an errorContext state at the root of my tree. The error context looks like so:
// ErrorContext.js
import React from 'react';
const ErrorContext = React.createContext({
isError: false,
setError: (error) => {}
});
export default ErrorContext;
Desired Result
I would like to update (consume) this context from anywhere in the app (specifically from within a promise)
Ideally the consume step should be extracted into a exported helper function
Example Usage of helper function
http.get('/blah')
.catch((error) => {
HelperLibrary.setError(true);
})
Following the react context docs:
I can create a provider like so :
class ProviderClass {
state = {
isError: false,
setError: (error) => {
this.state.isError = error;
}
}
render() {
return (
<ErrorContext.Provider value={this.state}>
{this.props.children}
</ErrorContext.Provider>
)
}
}
Then I can consume this provider by using the Consumer wrapper from inside a render call:
<ErrorContext.Consumer>
{(context) => {
context.setError(true);
}}
</ErrorContext.Consumer>
The Problem with this approach
This approach would require every developer on my team to write lots of boilerplate code every-time they wish to handle a web service error.
e.g. They would have to place ErrorContext.Consumer inside the components render() method and render it conditionally depending on the web service response.
What I have tried
Using ReactDOM.render from within a helper function.
const setError = (error) =>{
ReactDOM.render(
<ErrorContext.Consumer>
// boilerplate that i mentioned above
</ErrorContext.Consumer>,
document.getElementById('contextNodeInDOM')
) }
export default setError;
Why doesn't this work?
For some reason ReactDOM.render() always places this code outside the React component tree.
<App>
...
<ProviderClass>
...
<div id="contextNodeInDOM'></div> <-- even though my node is here
...
</ProviderClass>
</App>
<ErrorContext.Consumer></ErrorContext.Consumer> <-- ReactDOM.render puts the content here
Therefore there is no context parent found for the consumer, so it defaults to the default context (which has no state)
From the docs
If there is no Provider for this context above, the value argument
will be equal to the defaultValue that was passed to createContext().
If anyone can assist me on my next step, I am coming from Angular so apologies if my terminology is incorrect or if I am doing something extremely stupid.
You can export a HOC to wrap the error component before export, eliminating the boilerplate and ensuring that the context is provided only where needed, and without messing with the DOM:
// error_context.js(x)
export const withErrorContext = (Component) => {
return (props) => (
<ErrorContext.Consumer>
{context => <Component {...props} errorContext={context} />}
</ErrorContext.Consumer>
)
};
// some_component.js(x)
const SomeComponent = ({ errorContext, ...props }) => {
http.get('/blah')
.catch((error) => {
errorContext.setError(true);
})
return(
<div></div>
)
};
export default withErrorContext(SomeComponent);
Now that React 16.8 has landed you can also do this more cleanly with hooks:
const SomeComponent = props => {
const { setError } = useContext(ErrorContext)
http.get("/blah").catch(() => setError(true))
return <div />
}
Following the react context docs:
I can create a provider like so :
class ProviderClass {
state = {
isError: false,
setError: (error) => {
this.state.isError = error;
}
}
I don't think so - there should be setState used. There is a general rule in react "don't mutate state - use setState()" - abusing causes large part of react issues.
I have a feeling you don't understand context role/usage. This is more like a shortcut to global store eliminating the need of explicitly passing down props to childs through deep components structure (sometimes more than 10 levels).
App > CtxProvider > Router > Other > .. > CtxConsumer > ComponentConsumingCtxStorePropsNMethods
Accessing rendered DOM nodes with id is used in some special cases, generally should be avoided because following renders will destroy any changes made externally.
Use portals if you need to render sth somewhere outside of main react app html node.
I think this question has been answer several time but I can't find my specific case.
https://codesandbox.io/s/jjy9l3003
So basically I have an App component that trigger an action that change a state call "isSmall" to true if the screen is resized and less than 500px (and false if it is higher)
class App extends React.Component {
...
resizeHandeler(e) {
const { window, dispatch } = this.props;
if (window.innerWidth < 500 && !this.state.isSmall) {
dispatch(isSmallAction(true));
this.setState({ isSmall: true });
} else if (window.innerWidth >= 500 && this.state.isSmall) {
dispatch(isSmallAction(false));
console.log(isSmallAction(false));
this.setState({ isSmall: false })
}
};
componentDidMount() {
const { window } = this.props;
window.addEventListener('resize', this.resizeHandeler.bind(this));
}
...
I have an other component called HeaderContainer who is a child of App and connected to the Store and the state "isSmall", I want this component to rerender when the "isSmall" change state... but it is not
class Header extends React.Component {
constructor(props) {
super(props);
this.isSmall = props.isSmall;
this.isHome = props.isHome;
}
...
render() {
return (
<div>
{
this.isSmall
?
(<div>Is small</div>)
:
(<div>is BIG</div>)
}
</div>
);
}
...
even if I can see through the console that redux is actually updating the store the Header component is not re-rendering.
Can someone point out what I am missing ?
Am I misunderstanding the "connect()" redux-react function ?
Looking at your code on the link you posted your component is connected to the redux store via connect
const mapStateToProps = (state, ownProps) => {
return {
isHome: ownProps.isHome,
isSmall: state.get('isSmall')
}
}
export const HeaderContainer = connect(mapStateToProps)(Header);
That means that the props you are accessing in your mapStateToProps function (isHome and isSmall) are taken from the redux store and passed as props into your components.
To have React re-render your component you have to use 'this.props' inside the render function (as render is called every time a prop change):
render() {
return (
<div>
{
this.props.isSmall
?
(<div>Is small</div>)
:
(<div>is BIG</div>)
}
</div>
);
}
You are doing it well in the constructor but the constructor is only called once before the component is mounted. You should have a look at react lifecycle methods: https://reactjs.org/docs/react-component.html#constructor
You could remove entirely the constructor in your Header.js file.
You should also avoid using public class properties (e.g. this.isSmall = props.isSmall; ) in react when possible and make use of the React local state when your component needs it: https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class
A component is only mounted once and then only being updated by getting passed new props. You constructor is therefore only being called once before mount. That means that the instance properties you set there will never change during the lifetime of your mounted component. You have to directly Access this.props in your render() function to make updating work. You can remove the constructor as he doesn't do anything useful in this case.
I'm using React-Native-Router-Flux for routing my app. The issue is that it seems like when a redux state changes, ALL the components under the Router gets rerendered, not just the "current" component.
So lets say I have 2 components under the Router: Register and Login and both share the same authenticationReducer. Whenever an authentication event (such as user registration or signin) fails, I want to display error Alerts.
The problem is that when an error is fired from one of the components, two Alerts show up at the same time, one from each component. I assumed when I am currently on the Register scene, only the error alert would show from the Register component.
However, it seems like both components rerender whenever the redux state changes, and I see 2 alerts (In the below example, both 'Error from REGISTER' and 'Error from SIGNIN').
Here are the components:
main.ios.js
export default class App extends Component {
render() {
return (
<Provider store={store}>
<Router>
<Scene key='root'>
<Scene key='register' component={Register} type='replace'>
<Scene key='signin' component={SignIn} type='replace'>
</Scene>
</Router>
</Provider>
);
}
}
Register.js
class Register extends Component {
render() {
const { loading, error } = this.props;
if (!loading && error) {
Alert.alert('Error from REGISTER');
}
return <View>...</View>;
}
}
const mapStateToProps = (state) => {
return {
loading: state.get("authenticationReducer").get("loading"),
error: state.get("authenticationReducer").get("error"),
};
};
export default connect(mapStateToProps)(Register);
SignIn.js
class SignIn extends Component {
render() {
const { loading, error } = this.props;
if (!loading && error) {
Alert.alert('Error from SIGNIN');
}
return <View>...</View>;
}
}
const mapStateToProps = (state) => {
return {
loading: state.get("authenticationReducer").get("loading"),
error: state.get("authenticationReducer").get("error"),
};
};
export default connect(mapStateToProps)(SignIn);
How do I change this so that only the REGISTER error message shows when I am currently on the Register Scene, and vice versa?
Thanks
Because of the way react-native-router-flux works, all previous pages are still "open" and mounted. I am not totally sure if this solution will work, because of this weird quirk.
Pretty strict (and easy) rule to follow with React: No side-effects in render. Right now you are actually doing a side-effect there, namely, the Alert.alert(). Render can be called once, twice, whatever many times before actually rendering. This will, now, cause the alert to come up multiple times as well!
Try putting it in a componentDidUpdate, and compare it to the previous props to make sure it only happens once:
componentDidUpdate(prevProps) {
if (this.props.error && this.props.error !== prevProps.error) {
// Your alert code
}
}
I am not totally convinced that this will actually work, as the component will still update because it is kept in memory by react-native-router-flux, but it will at least have less quirks.
I solved this by creating an ErrorContainer to watch for errors and connected it to a component that uses react-native-simple-modal to render a single error modal throughout the app.
This approach is nice because you only need error logic and components defined once. The react-native-simple-modal component is awesomely simple to use too. I have an errors store that's an array that I can push errors to from anywhere. In the containers mapStateToProps I just grab the first error in the array (FIFO), so multiple error modals just "stack up", as you close one another will open if present.
container:
const mapStateToProps = (
state ) => {
return {
error: state.errors.length > 0 ? state.errors[0] : false
};
};
reducer:
export default function errors (state = [], action) {
switch (action.type) {
case actionTypes.ERRORS.PUSH:
return state.concat({
type: action.errorType,
message: action.message,
});
case actionTypes.ERRORS.POP:
return state.slice(1);
case actionTypes.ERRORS.FLUSH:
return [];
default:
return state;
}
}
I think we've been bitten by this issue (Strategy for avoiding cascading renders), but I don't know quite what to do about it.
In short, a child component is receiving property updates and executing a parent's bound render callback before the parent has received its prop updates. The result is that the callback uses stale properties and we get inconsistent rendering.
Here's a rough representation:
class Parent extends React.Component {
renderHeader = () => {
return <<uses this.props to render a header>>
}
render () {
return ( <Child renderHeader={this.renderHeader}/> )
}
}
function mapStateToProps(state) {
return Object.assign({}, state.tree.branch)
}
export default connect(mapStateToProps)(Parent);
...
class Child extends React.Component {
render () {
return (
...
this.props.renderHeader()
...
)
}
}
function mapStateToProps(state) {
return Object.assign({}, state.tree.branch)
}
export default connect(mapStateToProps)(Child);
I can make this work by passing props to the callback explicitly or by just avoid this pattern, but I feel like there's a larger concept/pattern/anti-pattern going on here that I'm failing to understand. Questions:
Is it actually invalid to connect both components to the state tree?
Is there clear logic for whether parents or children receive props first?