
Redux Example: Connect to Algorand Wallet via WalletConnect
dApps are often developed with a frontend framework like React or Vue.js. To let users interact with dApps using their wallet, users need to connect them. Every frontend component of the app should have access to the connected wallet’s information, such as the account balance. These components should also be able to submit transactions to the user’s wallet for review.
In this tutorial, we will go through how to connect to the Algorand Wallet using WalletConnect, and how to let every frontend component access the WalletConnect instance with the use of Redux and React Context.
You can download the source code for this application from the demo github repository.
You can try out the live demo app here.

Requirements
- IDE, e.g. VSCode
- NodeJS installed (latest version)
- JavaScript package manager - either yarn or npm which comes with NodeJS.
- Other dependencies are listed in package.json. Install them with
yarn install
ornpm install
.
Background
Connecting your Algorand mobile wallet to a dApp is simple. You only need to scan a QR code using the Algorand wallet app. This smooth user experience is made possible by the integration with WalletConnect. As developers, we are interested in knowing how we can capture the account data provided by the connected wallet, and how we can interact with the wallet on our dApp.
Algorand has provided a proof-of-concept example for us to see what interactions are possible between the React frontend and the connected wallet. The example is good for experiments yet doesn’t deal with how to share the wallet state across many components. Let’s learn how to do that with the help of Redux and React Context.
Steps
1. Create a React App
First of all, we need a brand new React project. The demo project was created using the create-react-app
command. You can learn more about this command here.
First, create react app from a template:
npx create-react-app algorand-wallet-walletconnect-redux
Next, we need to install the Redux toolkit separately.
yarn add @reduxjs/toolkit
Note
If you prefer to start with a template that includes Redux, you can run this instead:
npx create-react-app my-app --template redux
If you would like to start with a TypeScript template, run this:
npx create-react-app my-app --template redux-typescript
After creating our project from a template, let’s install some important packages for interacting with the Algorand blockchain and WalletConnect.
yarn add @walletconnect/client algorand-walletconnect-qrcode-modal algosdk @json-rpc-tools/utils
For the rest of the dependencies listed on package.json, you can run the following command if it is under “dependencies”.
yarn add package-name
Run the following command if it is under “devDependencies”.
yarn add package-name --dev
Note
Installing everything manually will take some time. Alternatively, you can clone the repository.
git clone https://github.com/fionnachan/algorand-wallet-walletconnect-redux.git
You can install all dependencies by running this command.
yarn install
Now, run the app for local development. It will automatically update when there are any changes.
yarn start
Let’s start coding!
2. Basic Layout
We need to create a basic layout for our app with multiple components. Here we are using the frontend component library Evergreen React UI, you can choose to use another library as you wish, e.g. Material UI.
src/App.tsx
...
const App: React.FC = () => {
const { isModalOpen } = useAppSelector((state) => state.application);
const dispatch = useAppDispatch();
const connector = useContext(ConnectContext);
...
return (
<div>
<div className="site-layout">
<SiteHeader />
<SiteBody />
<div className="footer">
Made with 💖 by{" "}
<a href="https://github.com/fionnachan" target="_blank" rel="noreferrer">
@fionnachan
</a>
</div>
<Dialog
isShown={isModalOpen}
title="Connect to a wallet"
hasFooter={false}
onCloseComplete={() => dispatch(setIsModalOpen(false))}
>
<Button className="wallet-button" onClick={connect}>
<img className="wallet-icon" src={algowallet} alt="Algorand wallet" />
<span>Algorand Wallet</span>
</Button>
</Dialog>
</div>
</div>
);
};
export default App;
The code here should be pretty straightforward. Both useAppDispatch
and useAppSelector
are the typed versions of the main React-Redux hooks we will be using in our React components. useSelector
accepts a selector function which extracts a state value from the Redux store. You can read more about useSelector
here.
useDispatch
is a proxy of the store.dispatch
method in our Redux store. We can pass in a reducer function to dispatch an action which will update the Redux state. You can read more about useDispatch
here.
Translating this into a React useState
hook equivalent, useSelector
allows us to access the state value, and useDispatch
allows us to set the state value by calling a reducer function.
useContext
is a hook to access a React context in any component. More on that soon.
In the above code snippet, setIsModalOpen
is a reducer function. We will have a look at our reducer functions for WalletConnect later.
Our connect function has three parts.
const connect = async () => {
if (connector.connected) return;
if (connector.pending) return QRCodeModal.open(connector.uri, null);
await connector.createSession();
};
First we check whether the connection between our dApp and the wallet is already established, if so there is no use connecting again.
If it isn’t we check if the connection between the dApp and the WalletConnect bridge - the websocket that lets a dApp and a wallet communicate - exists. If the dApp is already connected to the bridge we need to give the wallet the connection uri so it too can connect. This is done by opening the QRCodeModal
with the connector.uri
.
Lastly if the dApp is neither connected to the wallet nor to the bridge we call createSession()
src/store/connector.ts
import WalletConnect from "@walletconnect/client";
import { createContext } from "react";
import QRCodeModal from "algorand-walletconnect-qrcode-modal";
const connectProps = {
bridge: "https://bridge.walletconnect.org",
qrcodeModal: QRCodeModal,
};
export const connector = new WalletConnect(connectProps);
export const ConnectContext = createContext(connector);
This file is where we are instantiating our WalletConnect class and creating the React Context. A context is a nifty way to share data across many components without having to systematically pass it as props.
Our top-level App component is then sandwiched in the Redux store and the React context in src/index.tsx:
const renderApp = () =>
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<ConnectContext.Provider value={connector}>
<App />
</ConnectContext.Provider>
</Provider>
</React.StrictMode>,
document.getElementById("root"),
);
As a result our WalletConnect object will be accessible in every component with the following:
const connector = useContext(ConnectContext);
Note
A keen observer could say the connector object was already available without the context since we are exporting it from connector.ts
In our case passing the connector in a context ensures the components update if the WalletConnect instance mutates. We can also imagine advanced scenarios where the context would not hold one but an array of connectors as well as a function to instantiate new WalletConnect objects, for example to manage multiple wallets at once.
3. Creating the Site Header
We would like our users to click on a “Connect to a wallet” button to show the QR code for connection. In this tutorial, clicking the button on the header will bring up a modal - or dialog - to allow future integration with MyAlgo wallet and AlgoSigner.
src/components/SiteHeader/index.tsx
This file is pretty long, so we will break it down into several parts.
1. Imports
Among the first few imports are some helper functions. They are taken from this Algorand Wallet React app example. As for the long list of imports from the /features/
folder, they are our selector and reducer functions to be used with useAppSelector
and useAppDispatch
.
import React, { useContext, useEffect } from "react";
import { Button, Select } from "evergreen-ui";
import { ellipseAddress, formatBigNumWithDecimals } from "../../helpers/utilities";
import { IAssetData } from "../../helpers/types";
import {
reset,
onSessionUpdate,
getAccountAssets,
switchChain,
selectAssets,
} from "../../features/walletConnectSlice";
import { setIsModalOpen } from "../../features/applicationSlice";
import { ChainType } from "../../helpers/api";
import { useAppDispatch, useAppSelector } from "../../store/hooks";
import { ConnectContext } from "../../store/connector";
2. Set up selectors and a helper object
Right after creating our React component, we set up the constants we need.
const SiteHeader: React.FC = () => {
const { fetching: loading, address, chain } = useAppSelector((state) => state.walletConnect);
const assets = useAppSelector(selectAssets);
const dispatch = useAppDispatch();
const connector = useContext(ConnectContext);
...
}
3. Update state values in stores
connector
represents the WalletConnect
object. If we are connected to a wallet we dispatch the account address to the store. Then we subscribe to emitted events and perform actions accordingly. Finally we are returning a cleanup function.
useEffect(() => {
// Check if connection is already established
if (connector.connected) {
const { accounts } = connector;
dispatch(onSessionUpdate(accounts));
}
// Subscribe to connection events
console.log("%cin subscribeToEvents", "background: yellow");
connector.on("connect", (error, payload) => {
console.log("%cOn connect", "background: yellow");
if (error) {
throw error;
}
const { accounts } = payload.params[0];
dispatch(onSessionUpdate(accounts));
dispatch(setIsModalOpen(false));
});
connector.on("session_update", (error, payload) => {
console.log("%cOn session_update", "background: yellow");
if (error) {
throw error;
}
const { accounts } = payload.params[0];
dispatch(onSessionUpdate(accounts));
});
connector.on("disconnect", (error, payload) => {
console.log("%cOn disconnect", "background: yellow");
if (error) {
throw error;
}
dispatch(reset());
});
return () => {
console.log("%cin unsubscribeFromEvents", "background: yellow");
connector.off("connect");
connector.off("session_update");
connector.off("disconnect");
};
}, [dispatch, connector]);
If either the chain or the address is changed, we would do an asynchronous call to retrieve the account’s Algo and ASA balance.
useEffect(() => {
// Retrieve assets info
if (address?.length > 0) {
console.log("chain: ", chain);
dispatch(getAccountAssets({ chain, address }));
}
}, [dispatch, address, chain]);
4. The DOM
The following contains a Select dropdown for switching chains, a button for opening the modal, the amount of Algos the address has, the address, and a disconnect button.
const SiteHeader: React.FC = () => {
...
return (
<div className="site-layout-background site-header">
<div className="site-header-inner">
<div>
<span>Connected to </span>
<Select
value={chain}
onChange={(event) => dispatch(switchChain(event.target.value as ChainType))}
>
<option value={ChainType.TestNet}>Testnet</option>
<option value={ChainType.MainNet}>Mainnet</option>
</Select>
</div>
{!address ? (
<Button onClick={() => dispatch(setIsModalOpen(true))}>Connect Wallet</Button>
) : (
<div className="header-address-info">
{!loading && (
<span>
{formatBigNumWithDecimals(nativeCurrency.amount, nativeCurrency.decimals)}{" "}
{nativeCurrency.unitName || "units"}
</span>
)}
<span className="header-account">{ellipseAddress(address)}</span>
<Button
className="disconnect-button"
onClick={() => connector.killSession().catch((err) => console.error(err.message))}
>
Disconnect
</Button>
</div>
)}
</div>
</div>
);
};
export default SiteHeader;
4. Create the Site Body and the Asset Rows
In our app’s body, we are going to show all the assets of the account. These files are pretty clear to read given that you have a basic understanding of React.
src/components/SiteBody/index.tsx
import React from "react";
import { selectAssets } from "../../features/walletConnectSlice";
import { useAppSelector } from "../../store/hooks";
import AccountAssets from "../AccountAssets";
import LoadingIcon from "../LoadingIcon";
const SiteBody: React.FC = () => {
const loading = useAppSelector((state) => state.walletConnect.fetching);
const assets = useAppSelector(selectAssets);
return (
<div className="site-body">
<div className="site-body-inner">
{loading ? <LoadingIcon /> : <AccountAssets assets={assets} />}
</div>
</div>
);
};
export default SiteBody;
src/components/AccountAssets.tsx
import AssetRow from "./AssetRow";
import { IAssetData } from "../helpers/types";
const AccountAssets = ({ assets }: { assets: IAssetData[] }) => {
const nativeCurrency = assets.find((asset) => asset.id === 0)!;
const tokens = assets.filter((asset) => asset.id !== 0);
return (
<div>
<h2>Account Balance</h2>
<AssetRow key={nativeCurrency.id} asset={nativeCurrency} />
{tokens.map((token) => (
<AssetRow key={token.id} asset={token} />
))}
</div>
);
};
export default AccountAssets;
src/components/AssetRow.tsx
import Icon from "./Icon";
import ASAIcon from "./ASAIcon";
import algo from "../assets/algo.svg";
import { formatBigNumWithDecimals } from "../helpers/utilities";
import { IAssetData } from "../helpers/types";
const AssetRow = ({ asset }: { asset: IAssetData }) => (
<div className="asset-row">
<div className="asset-info">
{asset.id === 0 ? <Icon src={algo} /> : <ASAIcon assetID={asset.id} />}
<span>{asset.name}</span>
</div>
<div>
<div>
{`${formatBigNumWithDecimals(asset.amount as bigint, asset.decimals)} ${
asset.unitName || "units"
}`}
</div>
</div>
</div>
);
export default AssetRow;
6. Create the Reducer functions for the wallet
src/features/walletConnectSlice.ts
As this is the longest file we will go through, we will also study it in several sections.
1. Imports, TypeScript interface, and the inital state values
import { createAsyncThunk, createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { apiGetAccountAssets, ChainType } from "../helpers/api";
import { IAssetData } from "../helpers/types";
import { RootState } from "../store";
interface WalletConnectState {
chain: ChainType;
accounts: string[];
address: string;
assets: IAssetData[];
fetching: boolean;
}
const initialState = {
accounts: [],
address: "",
assets: [
{
id: 0,
amount: "0",
creator: "",
frozen: false,
decimals: 6,
name: "Algo",
unitName: "Algo",
},
],
chain: ChainType.TestNet,
fetching: false,
} as WalletConnectState;
2. createSlice
We use createSlice
to generate action creators and action types by passing in the initial state and reducer functions for our WalletConnect
related states. Learn more about createSlice
here.
export const walletConnectSlice = createSlice({
name: "walletConnect",
initialState,
reducers: {
switchChain(state, action: PayloadAction<ChainType>) {
state.chain = action.payload;
},
reset: (state) => ({ ...initialState, chain: state.chain }),
onSessionUpdate: (state, action: PayloadAction<string[]>) => {
state.accounts = action.payload;
state.address = action.payload[0];
},
},
extraReducers(builder) {
builder.addCase(getAccountAssets.fulfilled, (state, action) => {
state.fetching = false;
state.assets = action.payload;
});
builder.addCase(getAccountAssets.pending, (state) => {
state.fetching = true;
});
},
});
3. Handling an asynchronous call and saving its response as state value
This is the only asynchronous call we are performing in this tutorial. We will have to use createAsyncThunk
to ensure the state value is updated at the right time of the promise lifecycle, i.e. the state value assets
is updated when this call is successful and returns the response. This async thunk is used together with the extraReducers
above. Learn more about createAsyncThunk
here.
export const getAccountAssets = createAsyncThunk(
"walletConnect/getAccountAssets",
async ({ chain, address }: { chain: ChainType; address: string }) => {
return await apiGetAccountAssets(chain, address);
},
);
4. Selector function
Here we make a custom selector to convert the asset amounts from string back to BigInts. BigInts and all other non serializable objects should not be stored as is in the Redux store and were therefore converted to string in the api callback above.
export const selectAssets = createSelector(
(state: RootState) => state.walletConnect.assets,
(assets) => assets.map((a) => ({ ...a, amount: BigInt(a.amount) })),
);
5. Exports
We finally export the actions generated by createSlice
to use in the rest of the code base.
export const { switchChain, reset, onSessionUpdate } = walletConnectSlice.actions;
export default walletConnectSlice.reducer;
5. Create the Reducer function for the modal
After going through the reducer functions for the wallet, you should be able to read this file without any difficulties. We are handling the open/close state of the connect wallet modal in this file.
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
const initialState = {
isModalOpen: false,
};
export const applicationSlice = createSlice({
name: "application",
initialState,
reducers: {
setIsModalOpen: (state, action: PayloadAction<boolean>) => {
state.isModalOpen = action.payload;
},
},
});
export const { setIsModalOpen } = applicationSlice.actions;
export default applicationSlice.reducer;
7. Configure the Store
We need to set up the store in order to use it in our app. You can add some inital state values to preloadedState
if needed. A common example for preloaded state would be the user’s UI preference, e.g. light/dark mode. Learn more about configureStore
here.
import { configureStore } from "@reduxjs/toolkit";
import walletConnectReducer from "../features/walletConnectSlice";
import applicationReducer from "../features/applicationSlice";
import logger from "../features/logger";
const store = configureStore({
reducer: {
walletConnect: walletConnectReducer,
application: applicationReducer,
},
preloadedState: {},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
});
export type StoreGetSate = typeof store.getState;
export type RootState = ReturnType<StoreGetSate>;
export type AppDispatch = typeof store.dispatch;
export default store;
8. Play with your app
Now try adding more functions to the template, or integrate with MyAlgo wallet or AlgoSigner.
Check out the GitHub repository for this tutorial if you haven’t.
You can try out the live demo app here.
Warning
This tutorial is meant for educational purposes. It cannot be used in production without modification.