Skip to main content

Creating a themeable component

React version

In this guide we're going to be using React hooks added in React v16.8. If you're using the old demo-v1 you will have to convert these to lifecycle functions (e.g. componentWillMount).

In this guide we are going to create a themeable component.

We're going to add a countdown component.

00:00:30 until sale begins

1. Create your component

Create the file client/src/components/Countdown.js

Below is the basics of a countdown component (you can see a working example above).

client/src/components/Countdown.js

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

// Change the time from Milliseconds to a useful string
const formatTime = timeInMs => {
let seconds = Math.floor((timeInMs / 1000) % 60),
minutes = Math.floor((timeInMs / (1000 * 60)) % 60),
hours = Math.floor((timeInMs / (1000 * 60 * 60)) % 24);

hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;

return hours + ":" + minutes + ":" + seconds;
}

export const Countdown = ({ seconds }) => {
const millisecond = 1000,
startTime = seconds ? seconds * millisecond : millisecond * 5,
[time, setTime] = useState(startTime),
[isActive, setIsActive] = useState(true);

useLayoutEffect(() => {
let tick = setTimeout(() => {
if (time <= 0) {
setIsActive(false);
} else {
setTime(time - millisecond);
}
}, millisecond);

return () => {
clearTimeout(tick);
}
}, [time]);

return (
<div>
{isActive ?
<p>{`${formatTime(time)} until sale begins`}</p>
:
<p>Sale Started</p>
}
</div>
);
}

2. Export your component

Now you've created your component you will need to export it from the client/src/components/index.js file.

client/src/components/index.js

...
export * from './Countdown';

This simplifies the process of importing it in the future, by allowing it to be imported from the components folder directly rather than a subfile.

3. Include your component

The next step is to include the component somewhere on the page. We're going to add it to the header. Once you've done this you should see your component appear on the page...it will just be unstyled.

client/src/components/Header/HeaderMenuBar.js

...
import { Countdown } from '..';

export const HeaderMenuBar = props => {
...
return (
<Box>
...
<Countdown seconds={30} />
</Box>
);
};

4. Make your component 'themeable'

Let's head back to your component file client/src/components/Countdown.js and add our themed wrapper.

client/src/components/Countdown.js

...
import { Box, themed } from '@deity/falcon-ui';

const CountdownLayout = themed({
tag: Box,
defaultTheme: {
countdownLayout: {
p: 'md',
bg: 'primary',
css: ({ theme }) => ({
textAlign: 'center',
width: '100%',
p: {
color: theme.colors.secondary,
margin: 0,
fontSize: theme.fontSizes.lgTitle,
}
})
}
}
});

...

export const Countdown = ({ seconds }) => {
...
return (
<CountdownLayout>
{isActive ?
<p>{`${formatTime(time)} until sale begins`}</p>
:
<p>Sale Started</p>
}
</CountdownLayout>
);
}

We've now imported both Box & themed. We then create a new component CountdownLayout that is themed.

We can call this whatever we want but our naming convention suggests it should have a Layout suffix. e.g. [ComponentName]Layout.

We can then style this using the variables defined in our client/src/styling/theme.js.

5. Add translations

One last bit of tidy up here is we've got text that can't be translated.

Using the @deity/falcon-i18n package we can easily make these string translatable.

client/src/components/Countdown.js

import { T } from '@deity/falcon-i18n';

...
<CountdownLayout>
{isActive ?
<p><T id="countdown.message" time={formatTime(time)} /></p>
:
<p><T id="countdown.started" /></p>
}
</CountdownLayout>
...

client/i18n/en/translations.json

{
...
"countdown": {
"message": "{{time}} until sale starts",
"started": "Sale Started"
}
}

6. Code Cleanup

For extra brownie points it would be great to use our themeable Text component rather than <p> tags. This will mean it's both themeable and will adopt default styles already assigned to the <Text> component.

client/src/components/Countdown.js

...
import { Box, Text, themed } from '@deity/falcon-ui';
...

export const Countdown = ({ seconds }) => {
...
return (
<CountdownLayout>
{isActive ?
<Text><T id="countdown.message" time={formatTime(time)} /></Text>
:
<Text><T id="countdown.started" /></Text>
}
</CountdownLayout>
);
}

Finished

That's it...you've now got a fancy countdown component you can theme using your client/src/styling/theme.js or modify with our theme editor tool.

Full Code

Here's the complete Countdown.js file.

import React, { useState, useLayoutEffect } from 'react';
import { Box, Text, themed } from '@deity/falcon-ui';
import { T } from '@deity/falcon-i18n';

const CountdownLayout = themed({
tag: Box,
defaultTheme: {
countdownLayout: {
p: 'md',
bg: 'primary',
css: ({ theme }) => ({
textAlign: 'center',
width: '100%',
p: {
color: theme.colors.secondary,
margin: 0,
fontSize: theme.fontSizes.lgTitle,
}
})
}
}
});

// Change the time from Milliseconds to a useful string
const formatTime = timeInMs => {
let seconds = Math.floor((timeInMs / 1000) % 60),
minutes = Math.floor((timeInMs / (1000 * 60)) % 60),
hours = Math.floor((timeInMs / (1000 * 60 * 60)) % 24);

hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;

return hours + ":" + minutes + ":" + seconds;
}

export const Countdown = ({ seconds }) => {
const millisecond = 1000,
startTime = seconds ? seconds * millisecond : millisecond * 5,
[time, setTime] = useState(startTime),
[isActive, setIsActive] = useState(true);

useLayoutEffect(() => {
let tick = setTimeout(() => {
if (time <= 0) {
setIsActive(false);
} else {
setTime(time - millisecond);
}
}, 1000);

return () => {
clearTimeout(tick);
}
}, [time]);

return (
<CountdownLayout>
{isActive ?
<Text><T id="countdown.message" time={formatTime(time)} /></Text>
:
<Text><T id="countdown.started" /></Text>
}
</CountdownLayout>
);
}