Getting Started with Drizzle and React¶
Drizzle is the newest member of the Truffle Suite and our first front-end development tool. At its core, Drizzle takes care of synchronizing your contract data, transaction data and more from the blockchain to a Redux store. There are also higher-level abstractions on top of the base drizzle
library; tools for React compatibility (drizzle-react
) and a set of ready-to-use React components (drizzle-react-components
).
We're going to focus on the lower levels today, taking you through setting up a Truffle project with React and Drizzle from scratch. This way we can gain the best understanding of how Drizzle works under the hood. With this knowledge, you can leverage the full power of Drizzle with any front-end framework of your choosing, or use the higher-level React abstractions with confidence.
This will be a very minimal tutorial focused on setting and getting a simple string stored in a contract. It's meant for those with a basic knowledge of Truffle, who have some knowledge of JavaScript and React.js, but who are new to using Drizzle.
Note: For Truffle basics, please read through the Truffle Pet Shop tutorial before proceeding.
In this tutorial we will be covering:
- Setting up the development environment
- Creating a Truffle project from scratch
- Writing the smart contract
- Compiling and migrating the smart contract
- Testing the smart contract
- Creating our React.js project
- Wiring up the front-end client
- Wire up the React app with Drizzle
- Write a component to read from Drizzle
- Write a component to write to the smart contract
Setting up the development environment¶
There are a few technical requirements before we start. Please install the following:
- Node.js v8+ LTS and npm (comes with Node)
Truffle¶
Once we have those installed, we only need one command to install Truffle:
npm install -g truffle
To verify that Truffle is installed properly, type truffle version
on a terminal. If you see an error, make sure that your npm modules are added to your path.
Create-React-App¶
Finally, since this is a React.js tutorial, we will be creating our React project with Create-React-App.
You won't have to do anything if you have NPM version 5.2 or above. You can check your NPM version by running npm --version
. If you do not, then you will need to install the tool globally with this command:
npm install -g create-react-app
Creating a Truffle project¶
- Truffle initializes in the current directory, so first create a directory in your development folder of choice and then move inside it.
mkdir drizzle-react-tutorial
cd drizzle-react-tutorial
- Now we're ready to spawn our empty Truffle project by running the following command:
truffle init
Let's take a brief look at the directory structure that was just generated.
Directory structure¶
The default Truffle directory structure contains the following:
contracts/
: Contains the Solidity source files for our smart contracts. There is an important contract in here calledMigrations.sol
, which we'll talk about later.migrations/
: Truffle uses a migration system to handle smart contract deployments. A migration is an additional special smart contract that keeps track of changes.test/
: Contains both JavaScript and Solidity tests for our smart contracts.truffle-config.js
: Truffle configuration file.
Writing our smart contract¶
We'll add a simple smart contract called MyStringStore.
-
Create a new file named
MyStringStore.sol
in thecontracts/
directory. -
Add the following content to the file:
pragma solidity ^0.5.0;
contract MyStringStore {
string public myString = "Hello World";
function set(string memory x) public {
myString = x;
}
}
Since this isn't a Solidity tutorial, all you need to know about this is:
- We've created a public string variable named
myString
and initialized it to "Hello World". This automatically creates a getter (since it's a public variable) so we don't have to write one ourselves. - We've created a setter method that simply sets the
myString
variable with whatever string is passed in.
Launching a test blockchain with Truffle Develop¶
Before we move ahead, let's first launch our test blockchain with the Truffle Develop console.
Open up a new terminal, navigate to the project directory, and run the following command:
truffle develop
This will spawn a new blockchain that listens on 127.0.0.1:9545
by default.
Compiling and migrating the smart contract¶
Now we are ready to compile and migrate our contract.
Compilation¶
In the Truffle Develop console, type the following command:
compile
Note: If you're on Windows and encountering problems running this command, please see the documentation on [resolving naming conflicts on Windows](/docs/advanced/configuration#resolving-naming-conflicts-on-windows).
You should see output similar to the following:
Compiling ./contracts/Migrations.sol...
Compiling ./contracts/MyStringStore.sol...
Writing artifacts to ./build/contracts
Migration¶
Now that we've successfully compiled our contracts, it's time to migrate them to the blockchain!
Note: Read more about migrations in the [Truffle documentation](/docs/getting_started/migrations).
To create our own migration script.
-
Create a new file named
2_deploy_contracts.js
in themigrations/
directory. -
Add the following content to the
2_deploy_contracts.js
file:
const MyStringStore = artifacts.require("MyStringStore");
module.exports = function(deployer) {
deployer.deploy(MyStringStore);
};
- Back in our Truffle Develop console, migrate the contract to the blockchain.
migrate
You should see the migrations being executed in order, with the details of each migration listed.
Testing the smart contract¶
Before we proceed, we should write a couple tests to ensure that our contract works as expected.
-
Create a new file named
MyStringStore.js
in thetest/
directory. -
Add the following content to the
MyStringStore.js
file:
const MyStringStore = artifacts.require("./MyStringStore.sol");
contract("MyStringStore", accounts => {
it("should store the string 'Hey there!'", async () => {
const myStringStore = await MyStringStore.deployed();
// Set myString to "Hey there!"
await myStringStore.set("Hey there!", { from: accounts[0] });
// Get myString from public variable getter
const storedString = await myStringStore.myString.call();
assert.equal(storedString, "Hey there!", "The string was not stored");
});
});
Running the tests¶
- Back in the Truffle Develop console, run the tests:
test
- If all the tests pass, you'll see console output similar to this:
Using network 'develop'.
Contract: MyStringStore
✓ should store the string 'Hey there!' (98ms)
1 passing (116ms)
Awesome! Now we know that the contract actually works.
Creating our React.js project¶
Now that we are done with the smart contract, we can write our front-end client with React.js! In order to do this, open another terminal, navigate to your project directory, and simply run this command (if you have NPM version 5.2 or above):
npx create-react-app client
If you have an older version of NPM, make sure Create-React-App is installed globally as per the instructions in the Setting up the development environment section and then run the following:
create-react-app client
This should create a client
directory in your Truffle project and bootstrap a barebones React.js project for you to start building your front-end with.
Wiring up the front-end client¶
Since Create-React-App's default behavior disallows importing files from outside of the src
folder, we need to bring the contracts in our build
folder inside src
. We can copy and paste them every time we compile our contracts, but a better way is to simply configure Truffle to put the files there.
In the truffle-config.js
file, replace the contents with the following:
const path = require("path");
module.exports = {
contracts_build_directory: path.join(__dirname, "client/src/contracts")
};
This will make sure to output the contract build artifacts directory inside your React project. But this also means we'll have to restart our Truffle Develop console. Press CTRL + C
to exit out of the Truffle Develop console and then start it again with truffle develop
.
From there, make sure you run the compile
and migrate
commands again so that the new build artifacts will be output into the new folder. If you are encountering issues, try migrate --reset
for a clean migration from scratch.
Install Drizzle¶
This is the most delicious part, we install Drizzle. Make sure you are in the client
directory and then run the following:
npm install @drizzle/store
And that's it for dependencies! Note that we don't need to install Web3.js or @truffle/contract ourselves. Drizzle contains everything we need to work reactively with our smart contracts.
Wire up the React app with Drizzle¶
Before we go further, let's start our React app by running the follow command inside our client
directory:
npm start
This will serve the front-end under localhost:3000
, so open that up in your browser.
Note: Make sure to use an incognito window if you already have MetaMask installed (or disable MetaMask for now). Otherwise, the app will try to use the network specified in MetaMask rather than the develop network under `localhost:9545`.
If the default Create-React-App page loaded without any issues, you may proceed.
Setup the store¶
The first thing we need to do is to setup and instantiate the Drizzle store. We are going add the following code to client/src/index.js
:
// import drizzle functions and contract artifact
import { Drizzle } from "@drizzle/store";
import MyStringStore from "./contracts/MyStringStore.json";
// let drizzle know what contracts we want and how to access our test blockchain
const options = {
contracts: [MyStringStore],
web3: {
fallback: {
type: "ws",
url: "ws://127.0.0.1:9545",
},
},
};
// setup drizzle
const drizzle = new Drizzle(options);
First, we imported the tools from Drizzle as well as the contract definition.
We then built our options object for Drizzle, which in this case is just specifying the specific contract we want to be loaded by passing in the JSON build artifact.
And finally, we created the drizzleStore
and used that to create our drizzle
instance which we will pass in as a prop to our App
component.
Once that is complete, your index.js
should look something like this:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
// import drizzle functions and contract artifact
import { Drizzle, generateStore } from "@drizzle/store";
import MyStringStore from "./contracts/MyStringStore.json";
// let drizzle know what contracts we want and how to access our test blockchain
const options = {
contracts: [MyStringStore],
web3: {
fallback: {
type: "ws",
url: "ws://127.0.0.1:9545",
},
},
};
// setup the drizzle store and drizzle
const drizzle = new Drizzle(options);
ReactDOM.render(<App drizzle={drizzle} />, document.getElementById('root'));
Note again that the drizzle
instance is passed into the App
component as props.
Wire up the App component¶
Now that we have a drizzle
instance to play around with, we can go into client/src/App.js
to start working with the React API.
Adding state variables¶
First thing we will do is to add the following line inside our App component:
state = { loading: true, drizzleState: null };
We are going to be using two state variables here:
loading
— Indicates if Drizzle has finished initializing and the app is ready. The initialization process includes instantiatingweb3
and our smart contracts, fetching any available Ethereum accounts and listening (or, in cases where subscriptions are not supported: polling) for new blocks.drizzleState
— This is where we will store the state of the Drizzle store in our top-level component. If we can keep this state variable up-to-date, then we can simply use simpleprops
andstate
to work with Drizzle (i.e. you don't have to use any Redux or advanced React patterns).
Adding some initialization logic¶
Next we will add in our componentDidMount
method into the component class so that we can run some initialization logic.
componentDidMount() {
const { drizzle } = this.props;
// subscribe to changes in the store
this.unsubscribe = drizzle.store.subscribe(() => {
// every time the store updates, grab the state from drizzle
const drizzleState = drizzle.store.getState();
// check to see if it's ready, if so, update local component state
if (drizzleState.drizzleStatus.initialized) {
this.setState({ loading: false, drizzleState });
}
});
}
First, we grab the drizzle
instance from the props, then we call drizzle.store.subscribe
and pass in a callback function. This callback function is called whenever the Drizzle store is updated. Note that this store is actually a Redux store under the hood, so this might look familiar if you've used Redux previously.
Whenever the store is updated, we will try to grab the state with drizzle.store.getState()
and then if Drizzle is initialized and ready, we will set loading
to false, and also update the drizzleState
state variable.
By doing this, drizzleState
will always be up-to-date and we also know exactly when Drizzle is ready so we can use a loading component to let the user know.
Unsubscribing from the store¶
Note that we assign the return value of the subscribe()
to a class variable this.unsubscribe
. This is because it is always good practice to unsubscribe from any subscriptions you have when the component un-mounts. In order to do this, we save a reference to that subscription (i.e. this.unsubscribe
), and inside componentWillUnmount
, we have the following:
componentWillUnmount() {
this.unsubscribe();
}
This will safely unsubscribe when the App component un-mounts so we can prevent any memory leaks.
Replace the render
method¶
Finally, we can replace the boilerplate render method with something that applies to us better:
render() {
if (this.state.loading) return "Loading Drizzle...";
return <div className="App">Drizzle is ready</div>;
}
In the next section, we will replace "Drizzle is ready" with an actual component that will read from the store. If you refresh your browser and run this app now, you should see "Loading Drizzle..." briefly flash on screen and then subsequently "Drizzle is ready".
Full component code¶
When you are done this section, your App
component should look like the following:
class App extends Component {
state = { loading: true, drizzleState: null };
componentDidMount() {
const { drizzle } = this.props;
// subscribe to changes in the store
this.unsubscribe = drizzle.store.subscribe(() => {
// every time the store updates, grab the state from drizzle
const drizzleState = drizzle.store.getState();
// check to see if it's ready, if so, update local component state
if (drizzleState.drizzleStatus.initialized) {
this.setState({ loading: false, drizzleState });
}
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
if (this.state.loading) return "Loading Drizzle...";
return <div className="App">Drizzle is ready</div>;
}
}
Write a component to read from Drizzle¶
First, let's create a new file at client/src/ReadString.js
and paste in the following:
import React from "react";
class ReadString extends React.Component {
componentDidMount() {
const { drizzle, drizzleState } = this.props;
console.log(drizzle);
console.log(drizzleState);
}
render() {
return <div>ReadString Component</div>;
}
}
export default ReadString;
And then inside App.js
, import the new component with this statement:
import ReadString from "./ReadString";
Now modify your App.js
render method so that we pass in the drizzle
instance from props as well as the drizzleState
from the component state:
render() {
if (this.state.loading) return "Loading Drizzle...";
return (
<div className="App">
<ReadString
drizzle={this.props.drizzle}
drizzleState={this.state.drizzleState}
/>
</div>
);
}
Go back to the browser and open up your console. You should see that the two console.log
statements are working and they are displaying both the drizzle
instance as well as a drizzleState
that is fully initialized.
What this tells us is that the drizzleState
we get in this component will always be fully ready once this component mounts. At this point, you can take some time to explore the drizzle
instance object as well as the drizzleState
object.
drizzle
instance and drizzleState
¶
For the most part, drizzleState
is there for you to read information from (i.e. contract state variables, return values, transaction status, account data, etc.), whereas the drizzle
instance is what you will use to actually get stuff done (i.e. call contract methods, the Web3 instance, etc.).
Wiring up the ReadString
component¶
Now that we have access to our drizzle
instance and the drizzleState
, we can put in the logic that allows us read the smart contract variable we are interested in. Here is what the full code of ReadString.js
should look like:
import React from "react";
class ReadString extends React.Component {
state = { dataKey: null };
componentDidMount() {
const { drizzle } = this.props;
const contract = drizzle.contracts.MyStringStore;
// let drizzle know we want to watch the `myString` method
const dataKey = contract.methods["myString"].cacheCall();
// save the `dataKey` to local component state for later reference
this.setState({ dataKey });
}
render() {
// get the contract state from drizzleState
const { MyStringStore } = this.props.drizzleState.contracts;
// using the saved `dataKey`, get the variable we're interested in
const myString = MyStringStore.myString[this.state.dataKey];
// if it exists, then we display its value
return <p>My stored string: {myString && myString.value}</p>;
}
}
export default ReadString;
If everything is working, your app should display "Hello World". But first, let's walk through what we did here.
When the component mounts¶
componentDidMount() {
const { drizzle } = this.props;
const contract = drizzle.contracts.MyStringStore;
// let drizzle know we want to watch the `myString` method
const dataKey = contract.methods["myString"].cacheCall();
// save the `dataKey` to local component state for later reference
this.setState({ dataKey });
}
When the component mounts, we first grab a reference to the contract we are interested in and assign it to contract
.
We then need to tell Drizzle to keep track of a variable we are interested in. In order to do that, we call the .cacheCall()
function on the myString
getter method.
What we get in return is a dataKey
that allows us to reference this variable. We save this to the component's state so we can use it later.
The render
method¶
render() {
// get the contract state from drizzleState
const { MyStringStore } = this.props.drizzleState.contracts;
// using the saved `dataKey`, get the variable we're interested in
const myString = MyStringStore.myString[this.state.dataKey];
// if it exists, then we display its value
return <p>My stored string: {myString && myString.value}</p>;
}
From the drizzleState
, we grab the slice of the state we are interested in, which in this case is the MyStringStore
contract. From there, we use the dataKey
we saved from before in order to access the myString
variable.
Finally, we write myString && myString.value
to show the value of the variable if it exists, or nothing otherwise. And in this case, it should show "Hello World" since that is the string the contract is initialized with.
Quick Recap¶
The most important thing to get out of this section here is that there are two steps to reading a value with Drizzle:
- First, you need to let Drizzle know what variable you want to watch for. Drizzle will give you a
dataKey
in return and you need to save it for later reference. - Second, due to the asynchronous nature of how Drizzle works, you should be watching for changes in
drizzleState
. Once the variable accessed by thedataKey
exists, you will be able to get the value you are interested in.
Write a component to write to the smart contract¶
Of course, simply reading a pre-initialized variable is no fun at all; we want something that we can interact with. In this section, we will create an input box where you can type a string of your choice and have it save to the blockchain forever!
First, let's create a new file client/src/SetString.js
and paste in the following:
import React from "react";
class SetString extends React.Component {
state = { stackId: null };
handleKeyDown = e => {
// if the enter key is pressed, set the value with the string
if (e.keyCode === 13) {
this.setValue(e.target.value);
}
};
setValue = value => {
const { drizzle, drizzleState } = this.props;
const contract = drizzle.contracts.MyStringStore;
// let drizzle know we want to call the `set` method with `value`
const stackId = contract.methods["set"].cacheSend(value, {
from: drizzleState.accounts[0]
});
// save the `stackId` for later reference
this.setState({ stackId });
};
getTxStatus = () => {
// get the transaction states from the drizzle state
const { transactions, transactionStack } = this.props.drizzleState;
// get the transaction hash using our saved `stackId`
const txHash = transactionStack[this.state.stackId];
// if transaction hash does not exist, don't display anything
if (!txHash) return null;
// otherwise, return the transaction status
return `Transaction status: ${transactions[txHash] && transactions[txHash].status}`;
};
render() {
return (
<div>
<input type="text" onKeyDown={this.handleKeyDown} />
<div>{this.getTxStatus()}</div>
</div>
);
}
}
export default SetString;
At this point, import and include it inside App.js
just like you did with the ReadString
component:
import SetString from "./SetString";
// ...
render() {
if (this.state.loading) return "Loading Drizzle...";
return (
<div className="App">
<ReadString
drizzle={this.props.drizzle}
drizzleState={this.state.drizzleState}
/>
<SetString
drizzle={this.props.drizzle}
drizzleState={this.state.drizzleState}
/>
</div>
);
}
At this point, the app should work and you should try it out. You should be able to type something into the input text box, hit Enter, and Drizzle's react store will automatically display the new string.
Next, let's go through SetString.js
step-by-step.
General structure¶
First let's take a look at the general React.js boilerplate that we need.
class SetString extends React.Component {
state = { stackId: null };
handleKeyDown = e => {
// if the enter key is pressed, set the value with the string
if (e.keyCode === 13) {
this.setValue(e.target.value);
}
};
setValue = value => { ... };
getTxStatus = () => { ... };
render() {
return (
<div>
<input type="text" onKeyDown={this.handleKeyDown} />
<div>{this.getTxStatus()}</div>
</div>
);
}
}
In this component, we will have an input text box for the user to type in a string, and when the Enter key is pressed, the setValue
method will be called with the string as a parameter.
Also, we will display the status of the transaction. The getTxStatus
method will return a string displaying the status of the transaction by referencing a stackId
state variable (more on this later).
Submitting the transaction¶
setValue = value => {
const { drizzle, drizzleState } = this.props;
const contract = drizzle.contracts.MyStringStore;
// let drizzle know we want to call the `set` method with `value`
const stackId = contract.methods["set"].cacheSend(value, {
from: drizzleState.accounts[0]
});
// save the `stackId` for later reference
this.setState({ stackId });
};
We first assign the contract from the drizzle
instance into contract
, and then we call cacheSend()
on the method we are interested in (i.e. set
). Then we pass in the string we want to set (i.e. value
) as well as our transaction options (in this case, just the from
field). Note that we can get our current account address from drizzleState.accounts[0]
.
What we get in return is a stackId
, which is a reference to the transaction that we want to execute. Ethereum transactions don't receive a hash until they're broadcast to the network. In case an error occurs before broadcast, Drizzle keeps track of these transactions by giving each it's own ID. Once successfully broadcasted, the stackId
will point to the transaction hash, so we save it in our local component state for later usage.
Tracking transaction status¶
getTxStatus = () => {
// get the transaction states from the drizzle state
const { transactions, transactionStack } = this.props.drizzleState;
// get the transaction hash using our saved `stackId`
const txHash = transactionStack[this.state.stackId];
// if transaction hash does not exist, don't display anything
if (!txHash) return null;
// otherwise, return the transaction status
return `Transaction status: ${transactions[txHash] && transactions[txHash].status}`;
};
Now that we have a stackId
saved into our local component state, we can use this to check the status of our transaction. First, we need the transactions
and transactionStack
slices of state from drizzleState
.
Then, we can get the transaction hash (assigned to txHash
) via transactionStack[stackId]
. If the hash does not exist, then we know that the transaction has not been broadcasted yet and we return null.
Otherwise, we display a string to show the status of our transaction. Usually, this will either be "pending" or "success".
The End¶
Congratulations! You have taken a huge step to understanding how Drizzle works. Of course, this is only the beginning, you can use tools like drizzle-react
to help you integrate Drizzle into your dapp, reducing the necessary boilerplate that you would have to write.
Alternatively, you could also bootstrap your Drizzle dapp with our Truffle box.