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

Make React context in the example app work with hot module replacement #3462

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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