Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
Make React context in the example app work with hot module replacement
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed Oct 25, 2024
1 parent 99b7ff3 commit 658eadb
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 191 deletions.
67 changes: 6 additions & 61 deletions examples/react-app/src/context/ChainContext.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,22 @@
import type { ClusterUrl } from '@solana/web3.js';
import { devnet, mainnet, testnet } from '@solana/web3.js';
import React, { createContext, useMemo, useState } from 'react';
import { devnet } from '@solana/web3.js';
import { createContext } from 'react';

import { localStorage } from '../storage';

const STORAGE_KEY = 'solana-example-react-app:selected-chain';

type Context = Readonly<{
export type ChainContext = Readonly<{
chain: `solana:${string}`;
displayName: string;
setChain?(chain: 'solana:${string}'): void;
setChain?(chain: `solana:${string}`): void;
solanaExplorerClusterName: 'devnet' | 'mainnet-beta' | 'testnet';
solanaRpcSubscriptionsUrl: ClusterUrl;
solanaRpcUrl: ClusterUrl;
}>;

const DEFAULT_CHAIN_CONFIG = Object.freeze({
export const DEFAULT_CHAIN_CONFIG = Object.freeze({
chain: 'solana:devnet',
displayName: 'Devnet',
solanaExplorerClusterName: 'devnet',
solanaRpcSubscriptionsUrl: devnet('wss://api.devnet.solana.com'),
solanaRpcUrl: devnet('https://api.devnet.solana.com'),
});

export const ChainContext = createContext<Context>(DEFAULT_CHAIN_CONFIG);

export function ChainContextProvider({ children }: { children: React.ReactNode }) {
const [chain, setChain] = useState(() => localStorage.getItem(STORAGE_KEY) ?? 'solana:devnet');
const contextValue = useMemo<Context>(() => {
switch (chain) {
// @ts-expect-error Intentional fall through
case 'solana:mainnet':
if (process.env.REACT_EXAMPLE_APP_ENABLE_MAINNET === 'true') {
return {
chain: 'solana:mainnet',
displayName: 'Mainnet Beta',
solanaExplorerClusterName: 'mainnet-beta',
solanaRpcSubscriptionsUrl: mainnet('wss://api.mainnet-beta.solana.com'),
solanaRpcUrl: mainnet('https://api.mainnet-beta.solana.com'),
};
}
// falls through
case 'solana:testnet':
return {
chain: 'solana:testnet',
displayName: 'Testnet',
solanaExplorerClusterName: 'testnet',
solanaRpcSubscriptionsUrl: testnet('wss://api.testnet.solana.com'),
solanaRpcUrl: testnet('https://api.testnet.solana.com'),
};
case 'solana:devnet':
default:
if (chain !== 'solana:devnet') {
localStorage.removeItem(STORAGE_KEY);
console.error(`Unrecognized chain \`${chain}\``);
}
return DEFAULT_CHAIN_CONFIG;
}
}, [chain]);
return (
<ChainContext.Provider
value={useMemo(
() => ({
...contextValue,
setChain(chain) {
localStorage.setItem(STORAGE_KEY, chain);
setChain(chain);
},
}),
[contextValue],
)}
>
{children}
</ChainContext.Provider>
);
}
export const ChainContext = createContext<ChainContext>(DEFAULT_CHAIN_CONFIG);
57 changes: 57 additions & 0 deletions examples/react-app/src/context/ChainContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { mainnet, testnet } from '@solana/web3.js';
import { useMemo, useState } from 'react';

import { ChainContext, DEFAULT_CHAIN_CONFIG } from './ChainContext';

const STORAGE_KEY = 'solana-example-react-app:selected-chain';

export function ChainContextProvider({ children }: { children: React.ReactNode }) {
const [chain, setChain] = useState(() => localStorage.getItem(STORAGE_KEY) ?? 'solana:devnet');
const contextValue = useMemo<ChainContext>(() => {
switch (chain) {
// @ts-expect-error Intentional fall through
case 'solana:mainnet':
if (process.env.REACT_EXAMPLE_APP_ENABLE_MAINNET === 'true') {
return {
chain: 'solana:mainnet',
displayName: 'Mainnet Beta',
solanaExplorerClusterName: 'mainnet-beta',
solanaRpcSubscriptionsUrl: mainnet('wss://api.mainnet-beta.solana.com'),
solanaRpcUrl: mainnet('https://api.mainnet-beta.solana.com'),
};
}
// falls through
case 'solana:testnet':
return {
chain: 'solana:testnet',
displayName: 'Testnet',
solanaExplorerClusterName: 'testnet',
solanaRpcSubscriptionsUrl: testnet('wss://api.testnet.solana.com'),
solanaRpcUrl: testnet('https://api.testnet.solana.com'),
};
case 'solana:devnet':
default:
if (chain !== 'solana:devnet') {
localStorage.removeItem(STORAGE_KEY);
console.error(`Unrecognized chain \`${chain}\``);
}
return DEFAULT_CHAIN_CONFIG;
}
}, [chain]);
return (
<ChainContext.Provider
value={useMemo(
() => ({
...contextValue,
setChain(chain) {
localStorage.setItem(STORAGE_KEY, chain);
setChain(chain);
},
}),
[contextValue],
)}
>
{children}
</ChainContext.Provider>
);
}
26 changes: 1 addition & 25 deletions examples/react-app/src/context/RpcContext.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { Rpc, RpcSubscriptions, SolanaRpcApiMainnet, SolanaRpcSubscriptionsApi } from '@solana/web3.js';
import { createSolanaRpc, createSolanaRpcSubscriptions, devnet } from '@solana/web3.js';
import type { ReactNode } from 'react';
import { createContext, useContext, useMemo } from 'react';

import { ChainContext } from './ChainContext';
import { createContext } from 'react';

export const RpcContext = createContext<{
rpc: Rpc<SolanaRpcApiMainnet>; // Limit the API to only those methods found on Mainnet (ie. not `requestAirdrop`)
Expand All @@ -12,24 +9,3 @@ export const RpcContext = createContext<{
rpc: createSolanaRpc(devnet('https://api.devnet.solana.com')),
rpcSubscriptions: createSolanaRpcSubscriptions(devnet('wss://api.devnet.solana.com')),
});

type Props = Readonly<{
children: ReactNode;
}>;

export function RpcContextProvider({ children }: Props) {
const { solanaRpcSubscriptionsUrl, solanaRpcUrl } = useContext(ChainContext);
return (
<RpcContext.Provider
value={useMemo(
() => ({
rpc: createSolanaRpc(solanaRpcUrl),
rpcSubscriptions: createSolanaRpcSubscriptions(solanaRpcSubscriptionsUrl),
}),
[solanaRpcSubscriptionsUrl, solanaRpcUrl],
)}
>
{children}
</RpcContext.Provider>
);
}
26 changes: 26 additions & 0 deletions examples/react-app/src/context/RpcContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createSolanaRpc, createSolanaRpcSubscriptions } from '@solana/web3.js';
import { ReactNode, useContext, useMemo } from 'react';

import { ChainContext } from './ChainContext';
import { RpcContext } from './RpcContext';

type Props = Readonly<{
children: ReactNode;
}>;

export function RpcContextProvider({ children }: Props) {
const { solanaRpcSubscriptionsUrl, solanaRpcUrl } = useContext(ChainContext);
return (
<RpcContext.Provider
value={useMemo(
() => ({
rpc: createSolanaRpc(solanaRpcUrl),
rpcSubscriptions: createSolanaRpcSubscriptions(solanaRpcSubscriptionsUrl),
}),
[solanaRpcSubscriptionsUrl, solanaRpcUrl],
)}
>
{children}
</RpcContext.Provider>
);
}
109 changes: 7 additions & 102 deletions examples/react-app/src/context/SelectedWalletAccountContext.tsx
Original file line number Diff line number Diff line change
@@ -1,111 +1,16 @@
import type { UiWallet, UiWalletAccount } from '@wallet-standard/react';
import {
getUiWalletAccountStorageKey,
uiWalletAccountBelongsToUiWallet,
uiWalletAccountsAreSame,
useWallets,
} from '@wallet-standard/react';
import { createContext, useEffect, useMemo, useState } from 'react';
import type { UiWalletAccount } from '@wallet-standard/react';
import { createContext } from 'react';

import { localStorage } from '../storage';

type State = UiWalletAccount | undefined;

const STORAGE_KEY = 'solana-wallet-standard-example-react:selected-wallet-and-address';
export type SelectedWalletAccountState = UiWalletAccount | undefined;

export const SelectedWalletAccountContext = createContext<
readonly [selectedWalletAccount: State, setSelectedWalletAccount: React.Dispatch<React.SetStateAction<State>>]
readonly [
selectedWalletAccount: SelectedWalletAccountState,
setSelectedWalletAccount: React.Dispatch<React.SetStateAction<SelectedWalletAccountState>>,
]
>([
undefined /* selectedWalletAccount */,
function setSelectedWalletAccount() {
/* empty */
},
]);

let wasSetterInvoked = false;
function getSavedWalletAccount(wallets: readonly UiWallet[]): UiWalletAccount | undefined {
if (wasSetterInvoked) {
// After the user makes an explicit choice of wallet, stop trying to auto-select the
// saved wallet, if and when it appears.
return;
}
const savedWalletNameAndAddress = localStorage.getItem(STORAGE_KEY);
if (!savedWalletNameAndAddress || typeof savedWalletNameAndAddress !== 'string') {
return;
}
const [savedWalletName, savedAccountAddress] = savedWalletNameAndAddress.split(':');
if (!savedWalletName || !savedAccountAddress) {
return;
}
for (const wallet of wallets) {
if (wallet.name === savedWalletName) {
for (const account of wallet.accounts) {
if (account.address === savedAccountAddress) {
return account;
}
}
}
}
}

/**
* Saves the selected wallet account's storage key to the browser's local storage. In future
* sessions it will try to return that same wallet account, or at least one from the same brand of
* wallet if the wallet from which it came is still in the Wallet Standard registry.
*/
export function SelectedWalletAccountContextProvider({ children }: { children: React.ReactNode }) {
const wallets = useWallets();
const [selectedWalletAccount, setSelectedWalletAccountInternal] = useState<State>(() =>
getSavedWalletAccount(wallets),
);
const setSelectedWalletAccount: React.Dispatch<React.SetStateAction<State>> = setStateAction => {
setSelectedWalletAccountInternal(prevSelectedWalletAccount => {
wasSetterInvoked = true;
const nextWalletAccount =
typeof setStateAction === 'function' ? setStateAction(prevSelectedWalletAccount) : setStateAction;
const accountKey = nextWalletAccount ? getUiWalletAccountStorageKey(nextWalletAccount) : undefined;
if (accountKey) {
localStorage.setItem(STORAGE_KEY, accountKey);
} else {
localStorage.removeItem(STORAGE_KEY);
}
return nextWalletAccount;
});
};
useEffect(() => {
const savedWalletAccount = getSavedWalletAccount(wallets);
if (savedWalletAccount) {
setSelectedWalletAccountInternal(savedWalletAccount);
}
}, [wallets]);
const walletAccount = useMemo(() => {
if (selectedWalletAccount) {
for (const uiWallet of wallets) {
for (const uiWalletAccount of uiWallet.accounts) {
if (uiWalletAccountsAreSame(selectedWalletAccount, uiWalletAccount)) {
return uiWalletAccount;
}
}
if (uiWalletAccountBelongsToUiWallet(selectedWalletAccount, uiWallet) && uiWallet.accounts[0]) {
// If the selected account belongs to this connected wallet, at least, then
// select one of its accounts.
return uiWallet.accounts[0];
}
}
}
}, [selectedWalletAccount, wallets]);
useEffect(() => {
// If there is a selected wallet account but the wallet to which it belongs has since
// disconnected, clear the selected wallet.
if (selectedWalletAccount && !walletAccount) {
setSelectedWalletAccountInternal(undefined);
}
}, [selectedWalletAccount, walletAccount]);
return (
<SelectedWalletAccountContext.Provider
value={useMemo(() => [walletAccount, setSelectedWalletAccount], [walletAccount])}
>
{children}
</SelectedWalletAccountContext.Provider>
);
}
Loading

0 comments on commit 658eadb

Please sign in to comment.