Last active
January 8, 2022 14:20
-
-
Save Neo42/1ee4e06af4330fd961470ea8ae9d4a26 to your computer and use it in GitHub Desktop.
React Patterns: context module functions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Context Module Functions | |
import * as React from 'react' | |
import {dequal} from 'dequal' | |
import * as userClient from '../user-client' | |
import {useAuth} from '../auth-context' | |
const UserContext = React.createContext() | |
UserContext.displayName = 'UserContext' | |
function userReducer(state, action) { | |
switch (action.type) { | |
case 'start update': { | |
return { | |
...state, | |
user: {...state.user, ...action.updates}, | |
status: 'pending', | |
storedUser: state.user, | |
} | |
} | |
case 'finish update': { | |
return { | |
...state, | |
user: action.updatedUser, | |
status: 'resolved', | |
storedUser: null, | |
error: null, | |
} | |
} | |
case 'fail update': { | |
return { | |
...state, | |
status: 'rejected', | |
error: action.error, | |
user: state.storedUser, | |
storedUser: null, | |
} | |
} | |
case 'reset': { | |
return { | |
...state, | |
status: null, | |
error: null, | |
} | |
} | |
default: { | |
throw new Error(`Unhandled action type: ${action.type}`) | |
} | |
} | |
} | |
// wrap user state into a provider function with a value | |
function UserProvider({children}) { | |
const {user} = useAuth() | |
const [state, dispatch] = React.useReducer(userReducer, { | |
status: null, | |
error: null, | |
storedUser: user, | |
user, | |
}) | |
const value = [state, dispatch] | |
return <UserContext.Provider value={value}>{children}</UserContext.Provider> | |
} | |
// guard the context with a condition to tell if it's used within a provider | |
function useUser() { | |
const context = React.useContext(UserContext) | |
if (context === undefined) { | |
throw new Error(`useUser must be used within a UserProvider`) | |
} | |
return context | |
} | |
async function updateUser(dispatch, user, updates) { | |
dispatch({type: 'start update', updates}) | |
try { | |
const updatedUser = await userClient.updateUser(user, updates) | |
dispatch({type: 'finish update', updatedUser}) | |
return updatedUser | |
} catch (error) { | |
dispatch({type: 'fail update', error}) | |
return Promise.reject(error) | |
} | |
} | |
function UserSettings() { | |
const [{user, status, error}, userDispatch] = useUser() | |
const isPending = status === 'pending' | |
const isRejected = status === 'rejected' | |
const [formState, setFormState] = React.useState(user) | |
const isChanged = !dequal(user, formState) | |
function handleChange(e) { | |
setFormState({...formState, [e.target.name]: e.target.value}) | |
} | |
function handleSubmit(event) { | |
event.preventDefault() | |
updateUser(userDispatch, user, formState).catch(() => { | |
/* ignore the error */ | |
}) | |
} | |
return ( | |
<form onSubmit={handleSubmit}> | |
<div style={{marginBottom: 12}}> | |
<label style={{display: 'block'}} htmlFor="username"> | |
Username | |
</label> | |
<input | |
id="username" | |
name="username" | |
disabled | |
readOnly | |
value={formState.username} | |
style={{width: '100%'}} | |
/> | |
</div> | |
<div style={{marginBottom: 12}}> | |
<label style={{display: 'block'}} htmlFor="tagline"> | |
Tagline | |
</label> | |
<input | |
id="tagline" | |
name="tagline" | |
value={formState.tagline} | |
onChange={handleChange} | |
style={{width: '100%'}} | |
/> | |
</div> | |
<div style={{marginBottom: 12}}> | |
<label style={{display: 'block'}} htmlFor="bio"> | |
Biography | |
</label> | |
<textarea | |
id="bio" | |
name="bio" | |
value={formState.bio} | |
onChange={handleChange} | |
style={{width: '100%'}} | |
/> | |
</div> | |
<div> | |
<button | |
type="button" | |
onClick={() => { | |
setFormState(user) | |
userDispatch({type: 'reset'}) | |
}} | |
disabled={!isChanged || isPending} | |
> | |
Reset | |
</button> | |
<button | |
type="submit" | |
disabled={(!isChanged && !isRejected) || isPending} | |
> | |
{isPending | |
? '...' | |
: isRejected | |
? '✖ Try again' | |
: isChanged | |
? 'Submit' | |
: '✔'} | |
</button> | |
{isRejected ? <pre style={{color: 'red'}}>{error.message}</pre> : null} | |
</div> | |
</form> | |
) | |
} | |
function UserDataDisplay() { | |
const [{user}] = useUser() | |
return <pre>{JSON.stringify(user, null, 2)}</pre> | |
} | |
function App() { | |
return ( | |
<div | |
style={{ | |
minHeight: 350, | |
width: 300, | |
backgroundColor: '#ddd', | |
borderRadius: 4, | |
padding: 10, | |
}} | |
> | |
// use the provider here | |
<UserProvider> | |
<UserSettings /> | |
<UserDataDisplay /> | |
</UserProvider> | |
</div> | |
) | |
} | |
export default App |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment