Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delayed rendering of React components

I have a React component with a number of child components in it. I want to render the child components not at once but after some delay (uniform or different for each of the children).

I was wondering - is there a way how to do this?

like image 612
michalvalasek Avatar asked Jun 12 '15 12:06

michalvalasek


People also ask

How do you delay a component from rendering React?

To delay rendering of React components, we use the setTimeout function.

Why is my React component rendering so many times?

You can see in the console tab, that the render lifecycle got triggered more than once on both the app and greeting component. This is because the React app component got re-rendered after the state values were modified, and it also re-rendered its child components.

Why is React component not re rendering?

If React fails to do re-render components automatically, it's likely that an underlying issue in your project is preventing the components from updating correctly.


10 Answers

I think the most intuitive way to do this is by giving the children a "wait" prop, which hides the component for the duration that was passed down from the parent. By setting the default state to hidden, React will still render the component immediately, but it won't be visible until the state has changed. Then, you can set up componentWillMount to call a function to show it after the duration that was passed via props.

var Child = React.createClass({
    getInitialState : function () {
        return({hidden : "hidden"});
    },
    componentWillMount : function () {
        var that = this;
        setTimeout(function() {
            that.show();
        }, that.props.wait);
    },
    show : function () {
        this.setState({hidden : ""});
    },
    render : function () {
        return (
            <div className={this.state.hidden}>
                <p>Child</p>
            </div>
        )
    }
});

Then, in the Parent component, all you would need to do is pass the duration you want a Child to wait before displaying it.

var Parent = React.createClass({
    render : function () {
        return (
            <div className="parent">
                <p>Parent</p>
                <div className="child-list">
                    <Child wait={1000} />
                    <Child wait={3000} />
                    <Child wait={5000} />
                </div>
            </div>
        )
    }
});

Here's a demo

like image 198
Michael Parker Avatar answered Oct 12 '22 13:10

Michael Parker


Another approach for a delayed component:

Delayed.jsx:

import React from 'react';
import PropTypes from 'prop-types';

class Delayed extends React.Component {

    constructor(props) {
        super(props);
        this.state = {hidden : true};
    }

    componentDidMount() {
        setTimeout(() => {
            this.setState({hidden: false});
        }, this.props.waitBeforeShow);
    }

    render() {
        return this.state.hidden ? '' : this.props.children;
    }
}

Delayed.propTypes = {
  waitBeforeShow: PropTypes.number.isRequired
};

export default Delayed;

Usage:

 import Delayed from '../Time/Delayed';
 import React from 'react';

 const myComp = props => (
     <Delayed waitBeforeShow={500}>
         <div>Some child</div>
     </Delayed>
 )
like image 44
goulashsoup Avatar answered Oct 12 '22 13:10

goulashsoup


I have created Delayed component using Hooks and TypeScript

import React, { useState, useEffect } from 'react';

type Props = {
  children: React.ReactNode;
  waitBeforeShow?: number;
};

const Delayed = ({ children, waitBeforeShow = 500 }: Props) => {
  const [isShown, setIsShown] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => {
      setIsShown(true);
    }, waitBeforeShow);
    return () => clearTimeout(timer);
  }, [waitBeforeShow]);

  return isShown ? children : null;
};

export default Delayed;

Just wrap another component into Delayed

export const LoadingScreen = () => {
  return (
    <Delayed>
      <div />
    </Delayed>
  );
};
like image 43
Black Avatar answered Oct 12 '22 11:10

Black


In your father component <Father />, you could create an initial state where you track each child (using and id for instance), assigning a boolean value, which means render or not:

getInitialState() {
    let state = {};
    React.Children.forEach(this.props.children, (child, index) => {
        state[index] = false;
    });
    return state;
}

Then, when the component is mounted, you start your timers to change the state:

componentDidMount() {
    this.timeouts = React.Children.forEach(this.props.children, (child, index) => {
         return setTimeout(() => {
              this.setState({ index: true; }); 
         }, child.props.delay);
    });
}

When you render your children, you do it by recreating them, assigning as a prop the state for the matching child that says if the component must be rendered or not.

let children = React.Children.map(this.props.children, (child, index) => {
    return React.cloneElement(child, {doRender: this.state[index]});
});

So in your <Child /> component

render() {
    if (!this.props.render) return null;
    // Render method here
}

When the timeout is fired, the state is changed and the father component is rerendered. The children props are updated, and if doRender is true, they will render themselves.

like image 26
gcedo Avatar answered Oct 12 '22 13:10

gcedo


Using the useEffect hook, we can easily implement delay feature while typing in input field:

import React, { useState, useEffect } from 'react'

function Search() {
  const [searchTerm, setSearchTerm] = useState('')

  // Without delay
  // useEffect(() => {
  //   console.log(searchTerm)
  // }, [searchTerm])

  // With delay
  useEffect(() => {
    const delayDebounceFn = setTimeout(() => {
      console.log(searchTerm)
      // Send Axios request here
    }, 3000)

    // Cleanup fn
    return () => clearTimeout(delayDebounceFn)
  }, [searchTerm])

  return (
    <input
      autoFocus
      type='text'
      autoComplete='off'
      className='live-search-field'
      placeholder='Search here...'
      onChange={(e) => setSearchTerm(e.target.value)}
    />
  )
}

export default Search
like image 20
Harshal Avatar answered Oct 12 '22 13:10

Harshal


Depends on your use case.

If you want to do some animation of children blending in, use the react animation add-on: https://facebook.github.io/react/docs/animation.html Otherwise, make the rendering of the children dependent on props and add the props after some delay.

I wouldn't delay in the component, because it will probably haunt you during testing. And ideally, components should be pure.

like image 24
Markus Avatar answered Oct 12 '22 13:10

Markus


We can solve this using Hooks:

First we'll need a timeout hook for the delay.

This one is inspired by Dan Abramov's useInterval hook (see Dan's blog post for an in depth explanation), the differences being:

  1. we use we setTimeout not setInterval
  2. we return a reset function allowing us to restart the timer at any time

import { useEffect, useRef, useCallback } from 'react';

const useTimeout = (callback, delay) => {
  // save id in a ref
  const timeoutId = useRef('');

  // save callback as a ref so we can update the timeout callback without resetting the clock
  const savedCallback = useRef();
  useEffect(
    () => {
      savedCallback.current = callback;
    },
    [callback],
  );

  // clear the timeout and start a new one, updating the timeoutId ref
  const reset = useCallback(
    () => {
      clearTimeout(timeoutId.current);

      const id = setTimeout(savedCallback.current, delay);
      timeoutId.current = id;
    },
    [delay],
  );

  useEffect(
    () => {
      if (delay !== null) {
        reset();

        return () => clearTimeout(timeoutId.current);
      }
    },
    [delay, reset],
  );

  return { reset };
};

and now we need a hook which will capture previous children and use our useTimeout hook to swap in the new children after a delay

import { useState, useEffect } from 'react';

const useDelayNextChildren = (children, delay) => {
  const [finalChildren, setFinalChildren] = useState(children);

  const { reset } = useTimeout(() => {
    setFinalChildren(children);
  }, delay);

  useEffect(
    () => {
      reset();
    },
    [reset, children],
  );

  return finalChildren || children || null;
};

Note that the useTimeout callback will always have the latest children, so even if we attempt to render multiple different new children within the delay time, we'll always get the latest children once the timeout finally completes.

Now in your case, we want to also delay the initial render, so we make this change:

const useDelayNextChildren = (children, delay) => {
  const [finalChildren, setFinalChildren] = useState(null); // initial state set to null

  // ... stays the same

  return finalChildren || null;  // remove children from return
};

and using the above hook, your entire child component becomes

import React, { memo } from 'react';
import { useDelayNextChildren } from 'hooks';

const Child = ({ delay }) => useDelayNextChildren(
  <div>
    ... Child JSX goes here
    ... etc
  </div>
  , delay
);

export default memo(Child);

or if you prefer: ( dont say i havent given you enough code ;) )

const Child = ({ delay }) => {
  const render = <div>... Child JSX goes here ... etc</div>;

  return useDelayNextChildren(render, delay);
};

which will work exactly the same in the Parent render function as in the accepted answer

...

except the delay will be the same on every subsequent render too,

AND we used hooks, so that stateful logic is reusable across any component

...

...

use hooks. :D

like image 43
Matt Wills Avatar answered Oct 12 '22 12:10

Matt Wills


My use case might be a bit different but thought it might be useful to post a solution I came up with as it takes a different approach.

Essentially I have a third party Popover component that takes an anchor DOM element as a prop. The problem is that I cannot guarantee that the anchor element will be there immediately because the anchor element becomes visible at the same time as the Popover I want to anchor to it (during the same redux dispatch).

One possible fix was to place the Popover element deeper into the component tree than the element it was to be anchored too. However, that didn't fit nicely with the logical structure of my components.

Ultimately I decided to delay the (re)render of the Popover component a little bit to ensure that the anchor DOM element can be found. It uses the function as a child pattern to only render the children after a fixed delay:

import { Component } from 'react'
import PropTypes from 'prop-types'

export default class DelayedRender extends Component {
    componentDidMount() {
        this.t1 = setTimeout(() => this.forceUpdate(), 1500)
    }

    componentWillReceiveProps() {
        this.t2 = setTimeout(() => this.forceUpdate(), 1500)
    }

    shouldComponentUpdate() {
        return false
    }

    componentWillUnmount() {
        clearTimeout(this.t1)
        clearTimeout(this.t2)
    }

    render() {
        return this.props.children()
    }
}

DelayedRender.propTypes = {
    children: PropTypes.func.isRequired
}

It can be used like this:

<DelayedRender>
    {() =>
        <Popover anchorEl={getAnchorElement()}>
            <div>Hello!</div>
        </Popover>
    )}}
</DelayedRender>

Feels pretty hacky to me but works for my use case nevertheless.

like image 45
djskinner Avatar answered Oct 12 '22 11:10

djskinner


render the child components not at once but after some delay .

The question says delay render but if it is ok to render but hide...

You can render the components from a map straight away but use css animation to delay them being shown.

@keyframes Jumpin {
 0% { opacity: 0; }
 50% { opacity: 0; }
 100% { opacity: 1; }
}
// Sass loop code
@for $i from 2 through 10 {
 .div .div:nth-child(#{$i}) {
  animation: Jumpin #{$i * 0.35}s cubic-bezier(.9,.03,.69,.22);
 }
}

The child divs now follow each other with a slight delay.

like image 20
R.Meredith Avatar answered Oct 12 '22 13:10

R.Meredith


I have another here with a fallback option like Suspense

import { useState, useEffect } from "react";

export default function FakeSuspense(props) {
  const { children, delay, fallback } = props;
  const [isShown, setIsShown] = useState(false);

  useEffect(() => {
    setTimeout(() => {
      setIsShown(true);
    }, delay);
  }, [delay]);

  return isShown ? children : fallback;
}

then use it

<FakeSuspense delay={1700} fallback={<Spinner />}>
  <Component />
</FakeSuspense>
like image 41
inux Avatar answered Oct 12 '22 11:10

inux