Managing state is tough. Managing UI complexity is tough. Managing state in your app, on the Ethereum blockchain and handing the complexity of your UI is… tougher.
There are a few front-end architectures that have demonstrated the power of a model-view-update architecture in the last few years (Elm and om.next to name two) in keeping this code easy to reason about, and they lend themselves to an event driven model. Luckily, Solidity has events to track the changing state of a contract over time. Can we hook these things together? Let’s find out!
I’m using React with Redux, a library bringing the Elm architecture to plain javascript.
The flow of the app is split into
Once we’re up and running, we will
I’m just going to show the last two parts in this article; if you want to see all the code, you’ll want to look here.
const PayForm = ({ account, dispatch }) => ( | |
<Form | |
onSubmit={submittedValues => { | |
dispatch({ type: "pay", payee: account, ...submittedValues }); | |
}} | |
> | |
{formApi => ( | |
<form onSubmit={formApi.submitForm} id="pform"> | |
<label htmlFor="password">Password</label> | |
<Text field="password" id="password" /> | |
<button type="submit">Withdraw</button> | |
</form> | |
)} | |
</Form> | |
); |
You can see in this form the onSubmit
calls dispatch
with an action type of pay
and the values in our form.
const contractService = store => next => action => { | |
switch (action.type) { | |
... | |
case "pay": | |
contract | |
.withdraw(action.password, { from: action.payee }) | |
.catch(e => next(failed(e))); | |
... | |
default: | |
next(action); | |
} | |
}; |
The app has some custom middleware to catch this action and direct it to our contract. Unless the transaction fails, this is fire-and-forget; we’ll let the contract tell us when it’s done.
const eventToAction = name => (err, result) => { | |
store.dispatch({ type: "event", name: name, ...result.args }); | |
}; | |
contract.PaidEvent().watch(eventToAction("paid")); | |
contract.ReclaimedEvent().watch(eventToAction("reclaimed")); | |
contract.Instantiated().watch(eventToAction("created")); | |
contract.KilledStateChange().watch(eventToAction("killed")); |
When we started the UI; we watched for the events coming from the contract so we could dispatch them as actions.
const eventAction = (state = {}, action) => { | |
switch (action.type) { | |
... | |
case "event": | |
... | |
switch (action.name) { | |
case "created": | |
if (state.account == action.from) { | |
return { ...state, events: ev, status: "reclaimable" }; | |
} else { | |
return { ...state, events: ev, status: "payable" }; | |
} | |
case "paid": | |
return { ...state, events: ev, status: "" }; | |
... | |
} | |
}; |
This shows a snippet of our reducer. We are receiving the action from the contract and changing the state of the app to reflect this. The important state change is the status
. We see how this affects the UI below.
export const Forms = ({ status, killed, account, dispatch }) => { | |
if (killed) return <KillForm />; | |
return ( | |
<div> | |
{status ? <span /> : <CreateForm dispatch={dispatch} account={account} />} | |
{status == "payable" ? ( | |
<PayForm dispatch={dispatch} account={account} /> | |
) : ( | |
<span /> | |
)} | |
{status == "reclaimable" ? ( | |
<ReclaimForm dispatch={dispatch} account={account} /> | |
) : ( | |
<span /> | |
)} | |
<KillForm account={account} dispatch={dispatch} /> | |
</div> | |
); | |
}; |
Finally, which forms are displayed depends on that status; only when the status is payable
will we display the PayForm
. Our app is easy to reason about as it gets bigger; and nicely separated so we can write some tests to verify it behaves as it should for each state change.