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:
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.
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 prop
erty, 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.
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 P
rops, and S
tate. 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 P
rops interface
then the default is an empty object. Same for S
tate`.
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.