React and the Web Animation API

The web animation api is coming along and, with the polyfill, is supported in modern browsers. So, it seems like a good time to learn to integrate it into my current favorite front end library, React. But, the WAAPI works directly on DOM elements, which means it’s necessary to get access to them. This can be done in a couple ways.

Refs to Rendered Elements

If the component renders a DOM element directly, it’s easy enough to get a ref to the element.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class AnimatedDiv extends Component {
    componentDidMount() {
        if(this.ref) {
            this.ref.animate(
                [{opacity:0},{opacity:1}],
                {duration: 2000}
            );
        }
    }

    render() {
        return (
            <div style={{
                    height: 100,
                    width: 100, 
                    background: 'blue'
                }} 
                ref={(r) => this.ref=r}>
            </div>
        );
    }
}

ReactDOM.render(<AnimatedDiv />, document.getElementById('app'));

That’s the easy case out of the way, but what if the component is rendering another component? There are a few options, the first is using ReactDom.findDOMNode.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Div extends Component {
    render() {
        return (
            <div style={{
                    height: 100,
                    width: 100, 
                    background: 'blue'
                }} 
                ref={(r) => this.ref=r}>
            </div>
        );
    }
}

class AnimatedDiv extends Component {
    componentDidMount() {
        if(this.ref) {
            this._domNode = ReactDOM.findDOMNode(this.ref);
            this._domNode.animate(
                [{opacity:0},{opacity:1}],
                {duration: 2000}
            );
        }
    }

    render() {
        return <Div ref={(r) => this.ref=r} />;
    }
}

ReactDOM.render(<AnimatedDiv />, document.getElementById('app'));

It’s also possible to chain refs.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Div extends Component {
    getRef() {
        if (this.ref) return this.ref;

        return null;
    }

    render() {
        return (
            <div style={{
                    height: 100,
                    width: 100, 
                    background: 'blue'
                }} 
                ref={(r) => this.ref=r}>
            </div>
        );
    }
}

class AnimatedDiv extends Component {
    componentDidMount() {
        if(this.ref) {
            this._domNode = this.ref.getRef();
            this._domNode.animate(
                [{opacity:0},{opacity:1}],
                {duration: 2000}
            );
        }
    }

    render() {
        return <Div ref={(r) => this.ref=r} />;
    }
}

ReactDOM.render(<AnimatedDiv />, document.getElementById('app'));

Or pass a callback through props.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Div extends Component {
    render() {
        return (
            <div style={{
                    height: 100,
                    width: 100, 
                    background: 'blue'
                }} 
                ref={(r) => this.props.setRef(r)}>
            </div>
        );
    }
}

class AnimatedDiv extends Component {
    constructor() {
        super();
        this.setRef = this.setRef.bind(this);
    }

    componentDidMount() {
        if(this.ref) {
            this.ref.animate(
                [{opacity:0},{opacity:1}],
                {duration: 2000}
            );
        }
    }

    setRef(ref) {
        this.ref = ref;
    }

    render() {
        return <Div setRef={this.setRef} />;
    }
}

ReactDOM.render(<AnimatedDiv />, document.getElementById('app'));

Refs from Children

Instead of creating an animated component, we could instead create a component to apply animation to its children. We could do that for one child like this.

import React, { Component, Children } from 'react';
import ReactDOM from 'react-dom';

class Animation extends Component {

    componentDidMount() {
        if(this.ref) {
            this.ref.animate(
                [{opacity:0},{opacity:1}],
                {duration: 2000}
            );
        }
    }

    render() {
        let child = Children.map(this.props.children, (c) => (
            React.cloneElement(c, { ref: (r) => this.ref = r })
        ));
        
        return Children.only(child[0]);
    }
}

const AnimatedChildren = () => (
    <Animation>
        <div style={{
            height: 100,
            width: 100, 
            background: 'blue'
        }}>
        </div>
    </Animation>
);

ReactDOM.render(<AnimatedChildren/>, document.getElementById('app'));

Which is all well and good, but often children are generated dynamically based on data sets, so what about rendering multiple children?

import React, { Component, Children } from 'react';
import ReactDOM from 'react-dom';

class Animation extends Component {
    constructor() {
        super();

        this.childRefs = [];
    }

    componentDidMount() {

        this.childRefs.forEach( (ref) => (
            ref.animate(
                [{opacity:0},{opacity:1}],
                {duration: 2000}
            ))
        );
    
    }

    render() {
        let children = Children.map(this.props.children, (c, index) => (
            React.cloneElement(c, { ref: (r) => this.childRefs[index] = r })
        ));

        return (
            <div>
                {children}
            </div>
        );
    }
}

const AnimatedChildren = () => (
    <Animation>
        <div style={{
            height: 100,
            width: 100, 
            background: 'blue'
        }}>
        </div>
        <div style={{
            height: 100,
            width: 100, 
            background: 'blue'
        }}>
        </div>
        <div style={{
            height: 100,
            width: 100, 
            background: 'blue'
        }}>
        </div>
    </Animation>
);

ReactDOM.render(<AnimatedChildren/>, document.getElementById('app'));

Next Time

I think this post has gotten long enough. We’ve seen multiple ways to get references to the DOM elements required to make use of the WAAPI. Next time, I’ll go into using some of the more interesting parts of the API, like creating GroupEffects and SequenceEffects. As always, if anybody has corrections or tips, please leave a comment and let me know.