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 typescript enable console log](https://codereviewvideos.com/blog/wp-content/uploads/2018/08/console-log-typescript-adventure.png)
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 break all the things](https://codereviewvideos.com/blog/wp-content/uploads/2018/08/broken-during-typescript-refactoring.png)
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.
![typescript using react state typescript using react state](https://codereviewvideos.com/blog/wp-content/uploads/2018/08/this-state-colors-typescript-change.png)
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.