How, and Why


At this point we have a working website, but it doesn't do what we'd expect. All we've done is migrate some code over, and broken most of it :)

Let's continue on.

Here's our current code:

import * as React from 'react';
import './AnimatedBackground.css';

class AnimatedBackground extends React.Component {
    public render() {

        const colors = new Array(
            [62,35,255],
            [60,255,60],
            [255,35,98],
            [45,175,230],
            [255,0,255],
            [255,128,0]);

        let step = 0;
        const colorIndices = [0,1,2,3];

        const gradientSpeed = 0.002;

        function determineNextColourCombo(): [string,string]
        {
            const c00 = colors[colorIndices[0]];
            const c01 = colors[colorIndices[1]];
            const c10 = colors[colorIndices[2]];
            const c11 = colors[colorIndices[3]];

            const istep = 1 - step;
            const r1 = Math.round(istep * c00[0] + step * c01[0]);
            const g1 = Math.round(istep * c00[1] + step * c01[1]);
            const b1 = Math.round(istep * c00[2] + step * c01[2]);
            const color1 = "rgb("+r1+","+g1+","+b1+")";

            const r2 = Math.round(istep * c10[0] + step * c11[0]);
            const g2 = Math.round(istep * c10[1] + step * c11[1]);
            const b2 = Math.round(istep * c10[2] + step * c11[2]);
            const color2 = "rgb("+r2+","+g2+","+b2+")";

            return [color1, color2];
        }

        function updateGradient()
        {
            determineNextColourCombo();

            // $('#gradient').css({
            //     background: "-webkit-gradient(linear, left top, right top, from("+color1+"), to("+color2+"))"}).css({
            //     background: "-moz-linear-gradient(left, "+color1+" 0%, "+color2+" 100%)"});

            step += gradientSpeed;
            if ( step >= 1 )
            {
                step %= 1;
                colorIndices[0] = colorIndices[1];
                colorIndices[2] = colorIndices[3];

                colorIndices[1] = ( colorIndices[1] + Math.floor( 1 + Math.random() * (colors.length - 1))) % colors.length;
                colorIndices[3] = ( colorIndices[3] + Math.floor( 1 + Math.random() * (colors.length - 1))) % colors.length;

            }
        }

        setInterval(updateGradient,10);

        return (
            <div className="AnimatedBackground">
                hello
            </div>
        );
    }
}

export default AnimatedBackground;

It would be super helpful if we could validate something is happening with regards to the colour determination. However, if we try console.log then we will hit a linting issue:

+           console.log('colors', color1, color2);

            return [color1, color2];
        }

And as soon as we do, we can no longer compile:

Failed to compile.

/path/to/my/lovely-background-tutorial/src/AnimatedBackground.tsx
(38,13): Calls to 'console.log' are not allowed.

I think this rule is a little hardcore.

I'm going to disable this from stopping compilation by updating my tslint.json file. The thing is, I'm not convinced this is the best solution. I would like the no-console rule in production builds. However, I haven't figured that one out yet, so if you know a solution, please leave a comment below.

// ./tslint.json
{
  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
  "linterOptions": {
    "exclude": [
      "config/**/*.js",
      "node_modules/**/*.ts",
      "coverage/lcov-report/*.js"
    ]
  },
+ "rules": {
+   "no-console": false
+ }
}

As a heads up, you will need a restart / yarn start after this change.

Refreshing the page, and jumping into the dev tools > console, we're going to see hundreds of this:

typescript enable console log

Cool. It's working.

That's Not TypeScript!

Sticking a return type on a function, and being happy with nesting everything like this is possibly satisfactory to you. It's working in a way, but it's not nice code to work with. It should not be up to the render method to do all this stuff.

I will extract out the inner functions to private class methods. Nothing outside of the AnimatedBackground class should be able to determine the next colour combo.

break all the things

This is great. We've broken all the things.

Let's continue, moving updateGradient outside of the render method also.

We're now here:

import * as React from 'react';
import './AnimatedBackground.css';

class AnimatedBackground extends React.Component {
    public render() {

        const colors = new Array(
            [62,35,255],
            [60,255,60],
            [255,35,98],
            [45,175,230],
            [255,0,255],
            [255,128,0]);

        let step = 0;
        const colorIndices = [0,1,2,3];

        const gradientSpeed = 0.002;

        setInterval(this.updateGradient,10);

        return (
            <div className="AnimatedBackground">
                hello
            </div>
        );
    }

    private updateGradient()
    {
        this.determineNextColourCombo();

        // $('#gradient').css({
        //     background: "-webkit-gradient(linear, left top, right top, from("+color1+"), to("+color2+"))"}).css({
        //     background: "-moz-linear-gradient(left, "+color1+" 0%, "+color2+" 100%)"});

        step += gradientSpeed;
        if ( step >= 1 )
        {
            step %= 1;
            colorIndices[0] = colorIndices[1];
            colorIndices[2] = colorIndices[3];

            colorIndices[1] = ( colorIndices[1] + Math.floor( 1 + Math.random() * (colors.length - 1))) % colors.length;
            colorIndices[3] = ( colorIndices[3] + Math.floor( 1 + Math.random() * (colors.length - 1))) % colors.length;

        }
    }

    private determineNextColourCombo(): [string,string]
    {
        const c00 = colors[colorIndices[0]];
        const c01 = colors[colorIndices[1]];
        const c10 = colors[colorIndices[2]];
        const c11 = colors[colorIndices[3]];

        const istep = 1 - step;
        const r1 = Math.round(istep * c00[0] + step * c01[0]);
        const g1 = Math.round(istep * c00[1] + step * c01[1]);
        const b1 = Math.round(istep * c00[2] + step * c01[2]);
        const color1 = "rgb("+r1+","+g1+","+b1+")";

        const r2 = Math.round(istep * c10[0] + step * c11[0]);
        const g2 = Math.round(istep * c10[1] + step * c11[1]);
        const b2 = Math.round(istep * c10[2] + step * c11[2]);
        const color2 = "rgb("+r2+","+g2+","+b2+")";

        return [color1, color2];
    }
}

export default AnimatedBackground;

TypeScript is seemingly happy with our two private methods. However, it is less impressed with our variable / class property management:

Failed to compile.

/home/chris/Development/docker/lovely-background-tutorial/src/AnimatedBackground.tsx
(7,15): 'colors' is declared but its value is never read.

The State Of Things

Right at the top of the render method we have a bunch of const values being created. Every time the component is rendered, a new Array is assigned to colors, for example.

This array doesn't change. These are our background colour combinations, represented as rgb numbers.

We might want to make these into a property, so we can change them from outside the component. That's a further improvement, but for now, we are going to have these remain hard coded.

Even so, I don't want them to live in the render method. I want them to be available across the entire component. So I'm going to move them into the component's state.

    public constructor(props: any) {
        super(props);

        this.state = {
            colours: [
                [62,35,255],
                [60,255,60],
                [255,35,98],
                [45,175,230],
                [255,0,255],
                [255,128,0]
            ],
        };
    }

Any locations in code referring to colors now needs to use this.state.colours instead.

typescript using react state

Lots of errors.

The interesting one is:

"TS2339: Property colours does not exist on type ReadOnly<{}>"

What the heck is that all about?

TypeScript expects us to follow a set interface for any React.Component. Our AnimatedBackground extends React.Component. Therefore, our AnimatedBackground must contractually behave as expected.

Now, we didn't add any types to our AnimatedBackground class definition. Yet implicitly we still implemented the expected interface.

interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }

This interface states that we can provide objects representing our components Props, and State. If you're curious, SS stands for Snapshot. Snapshot is something that I've never needed to directly use.

If we don't define a more specific Props interface then the default is an empty object. Same for State`.

The error we're seeing is that colours does not exist as a property on {} (an empty object), which is kinda obvious when explained that way.

To fix this, we can either be really lazy:

class AnimatedBackground extends React.Component<any, any> {

Or do it properly:

type colourType = [number,number,number];

interface IGradientBgState {
    colours: colourType[],
}

class AnimatedBackground extends React.Component<any, IGradientBgState> {

We know a colour is an array of rgb values: [13, 179, 255]. I believe this is called a Triplet. A three element tuple. Please correct me if I am wrong.

We want our this.state.colours value to be an array of elements each with the 'shape' of a colourType.

Watch the video for a further exploration of this concept.

Even with this fix, we're not compiling:

Failed to compile.

/path/to/my/lovely-background-tutorial/src/AnimatedBackground.tsx
(13,13): 'step' is declared but its value is never read.

Let's continue to move these local variable definitions into the components state tree:

interface IGradientBgState {
+   colourIndices: number[],
    colours: colourType[],
+   gradientSpeed: number,
+   step: number,
}

class AnimatedBackground extends React.Component<any, IGradientBgState> {
    public render() {
-       let step = 0;
-       const colorIndices = [0,1,2,3];
-       const gradientSpeed = 0.002;

        setInterval(this.updateGradient,10);
        return (
            <div className="AnimatedBackground">
                hello
            </div>
        );
    }

    public constructor(props: any) {
        super(props);

        this.state = {
+           colourIndices: [0, 1, 2, 3],
            colours: [
                [13, 179, 255],
                [12, 105, 232],
                [0, 41, 255],
                [35, 12, 232],
                [105, 13, 255],
            ],
+           gradientSpeed: 0.002,
+           step: 0,
        };
    }

Every change we make to this.state needs to be reflected on the interface. gradientSpeed and step are simple number types, and colourIndices needs to be an array of those simple number types / number[].

Now we need to update any references to these variables with the this.state prefix:

import * as React from 'react';
import './AnimatedBackground.css';

type colourType = [number,number,number];

interface IGradientBgState {
    colourIndices: number[],
    colours: colourType[],
    gradientSpeed: number,
    step: number,
}

class AnimatedBackground extends React.Component<any, IGradientBgState> {
    public render() {

        setInterval(this.updateGradient,10);

        return (
            <div className="AnimatedBackground">
                hello
            </div>
        );
    }

    public constructor(props: any) {
        super(props);

        this.state = {
            colourIndices: [0, 1, 2, 3],
            colours: [
                [13, 179, 255],
                [12, 105, 232],
                [0, 41, 255],
                [35, 12, 232],
                [105, 13, 255],
            ],
            gradientSpeed: 0.002,
            step: 0,
        };
    }

    private updateGradient()
    {
        this.determineNextColourCombo();

        // $('#gradient').css({
        //     background: "-webkit-gradient(linear, left top, right top, from("+colour1+"), to("+colour2+"))"}).css({
        //     background: "-moz-linear-gradient(left, "+colour1+" 0%, "+colour2+" 100%)"});

        this.state.step += this.state.gradientSpeed;
        if ( this.state.step >= 1 )
        {
            this.state.step %= 1;
            this.state.colourIndices[0] = this.state.colourIndices[1];
            this.state.colourIndices[2] = this.state.colourIndices[3];

            this.state.colourIndices[1] = ( this.state.colourIndices[1] + Math.floor( 1 + Math.random() * (this.state.colours.length - 1))) % this.state.colours.length;
            this.state.colourIndices[3] = ( this.state.colourIndices[3] + Math.floor( 1 + Math.random() * (this.state.colours.length - 1))) % this.state.colours.length;
        }
    }

    private determineNextColourCombo(): [string,string]
    {
        const c00 = this.state.colours[this.state.colourIndices[0]];
        const c01 = this.state.colours[this.state.colourIndices[1]];
        const c10 = this.state.colours[this.state.colourIndices[2]];
        const c11 = this.state.colours[this.state.colourIndices[3]];

        const istep = 1 - this.state.step;
        const r1 = Math.round(istep * c00[0] + this.state.step * c01[0]);
        const g1 = Math.round(istep * c00[1] + this.state.step * c01[1]);
        const b1 = Math.round(istep * c00[2] + this.state.step * c01[2]);
        const colour1 = "rgb("+r1+","+g1+","+b1+")";

        const r2 = Math.round(istep * c10[0] + this.state.step * c11[0]);
        const g2 = Math.round(istep * c10[1] + this.state.step * c11[1]);
        const b2 = Math.round(istep * c10[2] + this.state.step * c11[2]);
        const colour2 = "rgb("+r2+","+g2+","+b2+")";

        return [colour1, colour2];
    }
}

export default AnimatedBackground;

Things are looking considerably more TypeScript-y at this point. But we aren't finished yet.

We have an immediate problem:

Failed to compile.

/path/to/my/lovely-background-tutorial/src/AnimatedBackground.tsx
(50,20): Cannot assign to 'step' because it is a constant or a read-only property.

Flippin' heck, right?

Will we ever be done. Soon. This is a two coffee kind of problem.

Don't Modify State Directly

We've broken a rule of React.

this.state.step += this.state.gradientSpeed;

If we modify this.state directly, React won't re-render. Instead, we must use the provided this.setState method:

        this.setState({
            step: this.state.step + this.state.gradientSpeed
        });

This is OK, but we can do a bit better.

We're using this.state.step in multiple places. Likewise, we're using this.state.colourIndices and this.state.colours multiple times, too.

Let's use ES6's lovely deconstruction functionality to make this entire block a little more terse:

    private updateGradient()
    {
        this.determineNextColourCombo();

        // $('#gradient').css({
        //     background: "-webkit-gradient(linear, left top, right top, from("+colour1+"), to("+colour2+"))"}).css({
        //     background: "-moz-linear-gradient(left, "+colour1+" 0%, "+colour2+" 100%)"});

        // deconstruction
        const {colours,gradientSpeed,step} = this.state;

        this.setState({
            step: step + gradientSpeed
        });

        if ( this.state.step >= 1 )
        {
            this.state.step %= 1;

            this.state.colourIndices[0] = this.state.colourIndices[1];
            this.state.colourIndices[2] = this.state.colourIndices[3];

            // pick two new target colour indices
            // do not pick the same as the current one
            this.state.colourIndices[1] = ( this.state.colourIndices[1] + Math.floor( 1 + Math.random() * (colours.length - 1))) % colours.length;
            this.state.colourIndices[3] = ( this.state.colourIndices[3] + Math.floor( 1 + Math.random() * (colours.length - 1))) % colours.length;
        }
    }

There's yet further problems here. Why are we calling the this.determineNextColourCombo(); function from inside the updateGradient method? We have three lines of comments / noise. And again, inside the conditional, we're trying to directly modify state.

I'm going to move the this.determineNextColourCombo(); outside of the updateGradient method:

    public render() {
+       this.determineNextColourCombo();
        setInterval(this.updateGradient,10);

        return (
            <div className="AnimatedBackground">
                hello
            </div>
        );
    }

// * snip *

    private updateGradient()
    {
-       this.determineNextColourCombo();

Not winning any awards for that one, but it's a better choice imo.

Then rather than modify this.state directly, again let's use this.setState appropriately:

-   this.state.step %= 1;

+   this.setState({
+       step: step % 1
+   });

Ok, so that all seems happy, but TypeScript is still unsatisfied:

Failed to compile.

/path/to/my/lovely-background-tutorial/src/AnimatedBackground.tsx
(25,5): Declaration of constructor not allowed after declaration of public instance method. Instead, this should come at the beginning of the class/interface.

Move the constructor method above the render method.

It's highly probably now that although your app compiles, it still blows up in 'production'.

TypeError: _a is undefined
./src/AnimatedBackground.tsx/AnimatedBackground.prototype.updateGradient
src/AnimatedBackground.tsx:48

  45 | //     background: "-webkit-gradient(linear, left top, right top, from("+colour1+"), to("+colour2+"))"}).css({
  46 | //     background: "-moz-linear-gradient(left, "+colour1+" 0%, "+colour2+" 100%)"});
  47 | 
> 48 | const {colours,gradientSpeed,step} = this.state;
  49 | 
  50 | this.setState({
  51 |     step: step + gradientSpeed

And this is all because we're using setInterval inside our render method. We can try a bit of hackery:

    public render() {
        this.determineNextColourCombo();
        setInterval(this.updateGradient.bind(this),2000);

        return (
            <div className="AnimatedBackground">
                hello
            </div>
        );
    }

It now works. But it doesn't follow React good practices. So let's continue on, and fix up the final few problems.

Code For This Course

Get the code for this course.

Code For This Video

Get the code for this video.

Episodes

# Title Duration
1 Super Mario 12:09
2 How, and Why 17:06
3 Finishing Up 06:10