Create a Front-end for the Kitties Chain
If you haven't completed Part I and just want to focus on building the Kitties front-end, clone the Kitty node solution from this branch and use that to follow this part of the tutorial.
In Part I we created all of the back-end portion of our Kitties application. In this part, it's time to build a user interface which can access and interact with our custom storage items and functions. We'll be using:
- Polkadot-JS API
- The Substrate Front-end Template, a React app that wraps Polkadot-JS API to make it easier to make RPC's to our chain's runtime.
- A library for generating Cat avatars, licensed under CC-By 4.0 attribution. Thank you David Revoy's for making this available.
In Part 2, there will only be two main sections: the first focusing on setting up the front-end template and the second focusing on building custom React components that can interact with our Kitty node.
In case you get stuck, the complete solution for this part of the tutorial can be found here.
Getting started
The first step of this tutorial is to familiarize yourself with the Substrate Front-end template. In this step we will go through an overview of what our React app will look like and the different components we'll be building. Start by installing the Front-end Template from your terminal:
git clone https://github.com/substrate-developer-hub/substrate-front-end-template.git
cd substrate-front-end-template
yarn install
Open the template in a code editor. You'll notice the following structure (we're only including the directories we care about for this tutorial):
substrate-front-end-template
|
+-- public
| |
| +-- assets <-- Kitty avatar PNG files
|
+-- src <-- our React components
| |
| +-- __tests__
| |
| +-- config <-- where to specify our custom types
| |
| +-- substrate-lib <-- lib to give access to PolkadotJS API
| | |
| | +-- components <-- contains TxButton, used throughout our application
| |
| ...
...
In a separate terminal, start an instance of node-kitties
that you built in Part I (or use the Kitty node
solution
instead if you haven't completed part I):
# Launch `node-kitties` from its directory.
cd kitties/
./target/release/node-kitties --dev --tmp
Now, in the same directory as where you installed the Front-end template, launch it:
yarn start
You should see a tab open up with the front-end template displaying basic features of your chain. Notice that it comes with a number of prebuilt components to provide basic interactions with a Substrate Node Template blockchain.
Sketching out our components
Substrate front-end template components use Polkadot-JS API and RPC endpoints to communicate with a Substrate node. This allows us to use it to read storage items, and make extrinsics by calling our pallet's dispatchable functions. Before we get to that, let's sketch out the different parts of our application.
We'll be building out a total of 3 components:
Kitties.js
: this will render the Kitty pane, and contains the logics of fetching all kitties information from the connecting Substrate node.KittyCards.js
: this will render a React card component containing a Kitty's relevant information, avatar and buttons to interact with it.KittyAvatar.js
: this will handle the logic to map Kitty DNA to the library of PNGs we're using to create unique Kitty avatars.
Polkadot JS API Basics
Before moving on to the next section, we reccommend you read a little Polkadot JS API documentation to understand the basics of how we will be querying storage and triggering transactions. Here are some good resources:
- Basics and Metadata
- RPC queries
- Storage methods such as
api.query.<pallet>.<method>
to access a pallet instance in a runtime - Extrinsics methods such as
api.tx.<pallet>.<method>
to trigger a transaction.
Creating custom components
Create Kitties.js
This is the component that will get rendered by Apps.js
, the top-most level component. So it does
the heavy lifting, with the help of KittyAvatar.js
and KittCards.js
.
Start by creating a file called
Kitties.js
insrc
and paste the following imports:import React, { useEffect, useState } from 'react' import { Form, Grid } from 'semantic-ui-react' import { useSubstrateState } from './substrate-lib' import { TxButton } from './substrate-lib/components' import KittyCards from './KittyCards'
The way our custom components make use of Polkadot-JS API is by using
substrate-lib
, which is a wrapper around Polkadot JS API instance and allows us to retrieve account keys from the Polkadot-JS keyring. This is why we useuseSubstrateState
which is exported bysrc/substrate-lib/SubstrateContext.js
and used to create the wrapper.Proceed by pasting in the following code snippet:
const parseKitty = ({ dna, price, gender, owner }) => ({ dna, price: price.toJSON(), gender: gender.toJSON(), owner: owner.toJSON(), }) export default function Kitties(props) { const { api, keyring } = useSubstrateState() const [kittyIds, setKittyIds] = useState([]) const [kitties, setKitties] = useState([]) const [status, setStatus] = useState('') // snip
The above code handles a few important things for our application:
parseKitty
is a function to hold all kitty data and return an object.Kitties
enables us to subscribe to chain storage item changes and use theuseEffect
React hook to update the state of our other components.
There are two things our app needs to subscribe to: storage changes in the amount of Kitties and changes in Kitty objects. To do this we'll create a subscription function for each.
We'll use
api.query.substrateKitties.countForKitties
to listen for a change in the amount of Kitties, which will queryCountForKitties
from our Kitties pallet storage item. Then, we'll use theapi.query.substrateKitties.kitties
method from Polkadot-JS API to get all kitty data and transform them with theparseKitty()
function.To enable this, paste the following snippet:
// Subscription function for kitty count const subscribeCount = () => { let unsub = null const asyncFetch = async () => { unsub = await api.query.substrateKitties.countForKitties(async count => { // Fetch all kitty keys const entries = await api.query.substrateKitties.kitties.entries() const ids = entries.map(entry => entry[1].unwrap().dna) setKittyIds(ids) }) } asyncFetch() return () => { unsub && unsub() } }
Similarly for
subscribeKitties
, paste the following code snippet:// Subscription function to construct all kitty objects const subscribeKitties = () => { let unsub = null const asyncFetch = async () => { unsub = await api.query.substrateKitties.kitties.multi( kittyIds, kitties => { const kittiesMap = kitties.map(kitty => parseKitty(kitty.unwrap())) setKitties(kittiesMap) } ) } asyncFetch() return () => { unsub && unsub() } }
kitties
is a storage map, Substrate supports returning the whole map of objects and we use Polkadot-JS API to iterate through the map. For each dataset, theparseKitty()
function is called to return a kitty object.Lastly, notice the clean-up function is being returned:
// return the unsubscription cleanup function return () => { unsub && unsub(); };
In
asyncFetch
we have subscribed to the Kitties storage. When the component is teared down, we want to make sure the subscription is cleaned up (unsubscribed). So we return a clean up function for the effect hook. Refer to Effects with Cleanup to learn more about cleanup functions.Now, all that's left to do in order for our component to listen for changes in our node's storage is to pass in the
subscribeCount
andsubscribeKitties
functions to React'suseEffect()
.Add these lines to enable this:
useEffect(subscribeCount, [api, keyring]) useEffect(subscribeKitties, [api, keyring, kittyIds])
Congratulations! We have setup the ground work of accessing the chain and saving all Kitty information
internally using React. We'll come back to the Kitties.js
component later once we create all the missing
components of our application to complete it.
Create KittyAvatar.js
In this component, all we're doing is mapping a library of PNG images to the bytes of our Kitty DNA. Since it's mostly all Javascript, we won't be going into much detail.
Create a file called
KittyAvatar.js
in thesrc
folder of your project and paste in the following code:import React from 'react' // Generate an array [start, start + 1, ..., end] inclusively const genArray = (start, end) => Array.from(Array(end - start + 1).keys()).map(v => v + start) const IMAGES = { accessory: genArray(1, 20).map( n => `${process.env.PUBLIC_URL}/assets/KittyAvatar/accessorie_${n}.png` ), body: genArray(1, 15).map( n => `${process.env.PUBLIC_URL}/assets/KittyAvatar/body_${n}.png` ), eyes: genArray(1, 15).map( n => `${process.env.PUBLIC_URL}/assets/KittyAvatar/eyes_${n}.png` ), mouth: genArray(1, 10).map( n => `${process.env.PUBLIC_URL}/assets/KittyAvatar/mouth_${n}.png` ), fur: genArray(1, 10).map( n => `${process.env.PUBLIC_URL}/assets/KittyAvatar/fur_${n}.png` ), } const dnaToAttributes = dna => { const attribute = (index, type) => IMAGES[type][dna[index] % IMAGES[type].length] return { body: attribute(0, 'body'), eyes: attribute(1, 'eyes'), accessory: attribute(2, 'accessory'), fur: attribute(3, 'fur'), mouth: attribute(4, 'mouth'), } } const KittyAvatar = props => { const outerStyle = { height: '160px', position: 'relative', width: '50%' } const innerStyle = { height: '150px', position: 'absolute', top: '3%', left: '50%', } const { dna } = props if (!dna) return null const cat = dnaToAttributes(dna) return ( <div style={outerStyle}> <img alt="body" src={cat.body} style={innerStyle} /> <img alt="fur" src={cat.fur} style={innerStyle} /> <img alt="mouth" src={cat.mouth} style={innerStyle} /> <img alt="eyes" src={cat.eyes} style={innerStyle} /> <img alt="accessory" src={cat.accessory} style={innerStyle} /> </div> ) } export default KittyAvatar
Notice that the only property being passed in is
dna
, which will be passed fromKittyCards.js
.The logic in this component is based on a specific Cat Avatar library of PNGs. Download it and paste the contents of
avatars/cat
inside a new folder calledKittyAvatar
under your project'spublic/assets
folder.Save and close
KittyAvatar.js
.
Write TransferModal
in KittyCards.js
Our KittyCards.js
component will have three sections to it:
TransferModal
,SetPrice
andBuyKitty
: each of which are modal dialogs that use theTxButton
component to transfer a kitty, set a kitty price, and to buy a kitty.KittyCard
: a card that renders the Kitty avatar using theKittyAvatar
component as well as all other Kitty information (id, dna, owner, gender and price).KittyCards
: a component that renders a grid forKittyCard
(yes, singular!) described above.
As a preliminary step, create a new file called
KittyCards.js
and add the following imports:import React from 'react' import { Button, Card, Grid, Message, Modal, Form, Label, } from 'semantic-ui-react' import KittyAvatar from './KittyAvatar' import { useSubstrateState } from './substrate-lib' import { TxButton } from './substrate-lib/components'
Let's outline what the
TransferModal
will do. Conveniently, the Substrate Front-end Template comes with a component calledTxButton
which is a useful way to include a transfer button that interacts with a node. This component will allow us to send a transaction into our node and trigger a signed extrinsic for the Kitties pallet.The way it is built can be broken down into the following pieces:
- A "transfer" button exists, which opens up a modal upon being clicked.
- This modal, we'll call "Kitty Transfer" is a
Form
containing (1) the Kitty ID and (2) an input field for a receiving address. - It also contains a "transfer" and "cancel" button.
See the screenshot for reference:
The other two modal dialogs,
SetPrice
andBuyKitty
, follow the similar logic.The first thing we'll do is to extract the properties (or "props") we need using React hooks. These are:
kitty
,accountPair
andsetStatus
. Do this by pasting in the following code snippet:const TransferModal = props => { const { kitty, accountPair, setStatus } = props; const [open, setOpen] = React.useState(false); const [formValue, setFormValue] = React.useState({}); const formChange = key => (ev, el) => { setFormValue({ ...formValue, [key]: el.value }); };
We also need a
confirmAndClose
function to be passed into theTxButton
component, being called when a confirmation action is triggered. This function will receive an unsubscription function fromTxButton
. In addition to calling this function for clean up, we will just close the modal dialog box. Paste the following snippet:const confirmAndClose = unsub => { setOpen(false) if (unsub && typeof unsub === 'function') unsub() }
Our Kitty Card has a "transfer" button that opens up a modal where a user can choose an address to send their Kitty to. That modal will have:
- a Title
- an read-only field for a Kitty ID
- an input field for an Account ID
- a "Cancel" button which closes the Transfer modal
- the
TxButton
React component to trigger the transaction
Paste this in to complete
TransferModal
and read the comments to follow what each piece of code is doing:return ( <Modal onClose={() => setOpen(false)} onOpen={() => setOpen(true)} open={open} trigger={ <Button basic color="blue"> Transfer </Button> } > <Modal.Header>Kitty Transfer</Modal.Header> <Modal.Content> <Form> <Form.Input fluid label="Kitty ID" readOnly value={kitty.dna} /> <Form.Input fluid label="Receiver" placeholder="Receiver Address" onChange={formChange('target')} /> </Form> </Modal.Content> <Modal.Actions> <Button basic color="grey" onClick={() => setOpen(false)}> Cancel </Button> <TxButton label="Transfer" type="SIGNED-TX" setStatus={setStatus} onClick={confirmAndClose} attrs={{ palletRpc: 'substrateKitties', callable: 'transfer', inputParams: [formValue.target, kitty.dna], paramFields: [true, true], }} /> </Modal.Actions> </Modal> )
The next part of our KittyCards.js
component is to create the part that renders the
KittyAvatar.js
component and the data passed in from the kitties
props in Kitty.js
.
Write KittyCard
in KittyCards.js
We'll use a Semantic UI Card
component to create a card that renders the Kitty avatar as well as the
Kitty DNA, gender, owner and price.
As you might have guessed, we'll use React props to pass in data to our KittyCard. Paste the following code snippet:
// Use props const KittyCard = props => { const { kitty, setStatus } = props const { dna = null, owner = null, gender = null, price = null } = kitty const displayDna = dna && dna.toJSON() const { currentAccount } = useSubstrateState() const isSelf = currentAccount.address === kitty.owner // snip
Write out the contents for the
Card
component. Paste the following and read the comments to understand what each line is doing:return ( <Card> {isSelf && ( <Label as="a" floating color="teal"> Mine </Label> )} {/* Render the Kitty Avatar */} <KittyAvatar dna={dna.toU8a()} /> <Card.Content> {/* Display the Kitty DNA */} <Card.Meta style={{ fontSize: '.9em', overflowWrap: 'break-word' }}> DNA: {displayDna} </Card.Meta> {/* Display the Kitty Gender, Owner, and Price */} <Card.Description> <p style={{ overflowWrap: 'break-word' }}>Gender: {gender}</p> <p style={{ overflowWrap: 'break-word' }}>Owner: {owner}</p> <p style={{ overflowWrap: 'break-word' }}> Price: {price || 'Not For Sale'} </p> </Card.Description> </Card.Content> // snip...
Before closing the
<Card/>
component we want to render theTransferModal
andSetPrice
components we previously built — only if the Kitty is transferrable by its owner. Paste this code snippet to handle this functionality:<Card.Content extra style={{ textAlign: 'center' }}> {owner === currentAccount.address ? ( <> <SetPrice kitty={kitty} setStatus={setStatus} /> <TransferModal kitty={kitty} setStatus={setStatus} /> </> ) : ( <> <BuyKitty kitty={kitty} setStatus={setStatus} /> </> )} </Card.Content> </Card>
It's time to put all the pieces we've built together. We need a function to:
- Check whether there are any Kitties to render and render a "No Kitty found here... Create one now!" message if there aren't any.
- If there are, render them in a 3 column grid.
Have a look at the comments to understand the parts of this code snippet and paste it in
KittyCards.js
:const KittyCards = props => { const { kitties, setStatus } = props if (kitties.length === 0) { return ( <Message info> <Message.Header> No Kitty found here... Create one now! <span role="img" aria-label="point-down"> 👇 </span> </Message.Header> </Message> ) } return ( <Grid columns={3}> {kitties.map((kitty, i) => ( <Grid.Column key={`kitty-${i}`}> <KittyCard kitty={kitty} setStatus={setStatus} /> </Grid.Column> ))} </Grid> ) }
Complete the component by adding:
export default KittyCards
Note: We didn't go through the
SetPrice
andBuyKitty
components above. Please refer to the solution code for learning how to write those.
Complete Kitties.js
Now that we've built all the bits for our front-end application, we can piece everything together.
Go back to the incompleted
Kitties.js
file and paste this code snippet to render theKittyCard.js
component inside a<Grid/>
:return <Grid.Column width={16}> <h1>Kitties</h1> <KittyCards kitties={kitties} setStatus={setStatus}/>
Now we'll use the
<Form/>
component to render our application'sTxButton
component. Paste in the following snippet:<Form style={{ margin: '1em 0' }}> <Form.Field style={{ textAlign: 'center' }}> <TxButton label='Create Kitty' type='SIGNED-TX' setStatus={setStatus} attrs={{ palletRpc: 'substrateKitties', callable: 'createKitty', inputParams: [], paramFields: [] }} /> </Form.Field> </Form> <div style={{ overflowWrap: 'break-word' }}>{status}</div> </Grid.Column>; }
Update App.js
Now all that's left to do is connect our custom components to the main application.
In
App.js
, import theKitties.js
component:import Kitties from './Kitties'
Finally, to render
Kitties.js
, add a new row to<Container/>
:<Grid.Row> <Kitties /> </Grid.Row>
If you get stuck in any of the above section. You can refer back to the complete source code of:
🎉*Congratulations!*🎉 You have finished the Substrate Kitties front-end tutorial! In our solution, we implemented the Set Price modal for you to use as an additional example. There are lots of ways you can extend what you've learnt here, such as adding a "Breed Kitty" component, creating a marketplace for different types of avatars, or recreating this front-end with your own UI components. We encourage you to keep learning!
Next steps
- Explore the Polkadot JS API cookbook
- Read about creating custom RPCs in Substrate
- Learn more about how "Effect Hooks" work in React's documentation