In this post, I'm going to build an address search component using React.
Requirements
In the component, I have a postcode value, and every time I click the search button, I can get the address.
It will have two cases of the result:
- Return one address.
- Return many addresses.
If the result is one address, I will set the value for the addresses inputs(disabled) otherwise I have to show the address list for the user. When the user selects an address from the address list, the selected address will be showed in the address's inputs.
Thinking in React
Components
My component will have 4 child components:
-
PostCode
. -
Search
. -
Addresses
. -
Address
.
State
This is some states I need to have for my components:
-
postCode
forPostCode
. -
addresses
forAddresses
. -
address
forAddress
. -
isOpen
for toggling theAddresses
component.
Implement
Coding
I will use Context
in order to avoid props drilling
in React.
-
Search
component need to use these state:postCode
,address
,addresses
,isOpen
. -
Addresses
component need to use state:address
.
So I will have the Providers
like below:
<AddressProvider>
<AddressesProvider>
<IsOpenProvider>
<PostCodeProvider>
<PostCode />
<Search />
</PostCodeProvider>
<Addresses />
</IsOpenProvider>
</AddressesProvider>
<Address />
</AddressProvider>
I created a function that helps me for generating the context and a hook
to consume that context.
function createContext(name: string) {
const context = React.createContext(null)
function useContext() {
const contextValue = React.useContext(context)
if (contextValue === null) {
throw new Error(`use${name} must be used within ${name}Provider`)
}
return contextValue
}
return {Context: context, useContext}
}
Now, I will create the provider above:
type IsOpenContextType = {
isOpen: boolean
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>
}
const {Context: IsOpenContext, useContext: useIsOpen} =
createContext<IsOpenContextType>('IsOpen')
function IsOpenProvider({children}: {children: React.ReactNode}) {
const [isOpen, setIsOpen] = React.useState(false)
const value = React.useMemo(() => ({isOpen, setIsOpen}), [isOpen, setIsOpen])
return (
<IsOpenContext.Provider value={value}>{children}</IsOpenContext.Provider>
)
}
type Address = {
code: number
prefecture: string
city: string
ward: string
}
type AddressContextType = {
address: Address | null
setAddress: React.Dispatch<React.SetStateAction<Address | null>>
}
const {Context: AddressContext, useContext: useAddress} =
createContext<AddressContextType>('Address')
function AddressProvider({children}: {children: React.ReactNode}) {
const [address, setAddress] = React.useState<Address | null>(null)
const value = React.useMemo(
() => ({address, setAddress}),
[address, setAddress],
)
return (
<AddressContext.Provider value={value}>{children}</AddressContext.Provider>
)
}
type AddressesContextType = {
addresses: Array<Address> | null
setAddresses: React.Dispatch<React.SetStateAction<Array<Address> | null>>
}
const {Context: AddressesContext, useContext: useAddresses} =
createContext<AddressesContextType>('Addresses')
function AddressesProvider({children}: {children: React.ReactNode}) {
const [addresses, setAddresses] = React.useState<Array<Address> | null>(null)
const value = React.useMemo(
() => ({addresses, setAddresses}),
[addresses, setAddresses],
)
return (
<AddressesContext.Provider value={value}>
{children}
</AddressesContext.Provider>
)
}
type PostCodeContextType = {
postCode: Array<string>
setPostCode: React.Dispatch<React.SetStateAction<Array<string>>>
}
const {Context: PostCodeContext, useContext: usePostCode} =
createContext<PostCodeContextType>('PostCode')
function PostCodeProvider({children}: {children: React.ReactNode}) {
const [postCode, setPostCode] = React.useState(() =>
Array.from({length: 2}, () => ''),
)
const value = React.useMemo(
() => ({postCode, setPostCode}),
[postCode, setPostCode],
)
return (
<PostCodeContext.Provider value={value}>
{children}
</PostCodeContext.Provider>
)
}
PostCode
component:
function PostCode() {
const {postCode, setPostCode} = usePostCode()
function hanldePostCodeChange(
event: React.ChangeEvent<HTMLInputElement>,
idx: number,
) {
const newPostCode = [...postCode]
newPostCode.splice(idx, 1, event.target.value)
setPostCode(newPostCode)
}
return (
<div>
<input onChange={(e) => hanldePostCodeChange(e, 0)} />
<input onChange={(e) => hanldePostCodeChange(e, 1)} />
</div>
)
}
When I click the search button, I have to fake an API for it. I will use msw
, and create a fake API.
rest.get('/addresses', (req, res, ctx) => {
return res(
ctx.delay(3000),
ctx.status(200),
ctx.json({
data: [
{
code: 13,
prefecture: 'Tokyo',
city: 'Otaku',
ward: 'Kamata',
},
{
code: 12,
prefecture: 'Osaka',
city: 'Namba',
ward: 'Suidou',
},
],
}),
)
}),
Search
component:
function Search() {
const {setAddress} = useAddress()
const {postCode} = usePostCode()
const {setAddresses} = useAddresses()
const {setIsOpen} = useIsOpen()
async function handleAddressesSearch() {
const query = postCode.every((pc) => Boolean(pc)) ? postCode.join('-') : ''
if (!query) return
const res = await fetch(`addresses?postCode=${query}`)
const resJson = await res.json()
if (resJson.data.length > 1) {
setIsOpen(true)
setAddresses(resJson.data)
} else {
setAddress(resJson.data[0])
}
}
return <button onClick={handleAddressesSearch}>Search</button>
}
Addresses
component:
function Addresses() {
const {addresses} = useAddresses()
const {setAddress} = useAddress()
const {isOpen, setIsOpen} = useIsOpen()
function handleAddressSelect(address: Address) {
setIsOpen(false)
setAddress(address)
}
if (!isOpen) return null
return (
<ul>
{addresses?.map((ad, _idx) => (
<li
key={`addresses-items-${_idx}`}
onClick={() => handleAddressSelect(ad)}
>
{ad.prefecture},{ad.city}, {ad.ward}
</li>
))}
</ul>
)
}
Address
component:
function Address() {
const {address, setAddress} = useAddress()
function handleWardChange(event: React.ChangeEvent<HTMLInputElement>) {
setAddress({
...address,
ward: event.target.value,
})
}
return (
<div>
<input value={address?.code ?? ''} disabled />
<input value={address?.prefecture ?? ''} disabled />
<input value={address?.city ?? ''} disabled />
<input value={address?.ward ?? ''} onChange={handleWardChange} />
</div>
)
}
And combine all of the components to my page:
function AutoAddress() {
return (
<AddressProvider>
<AddressesProvider>
<IsOpenProvider>
<PostCodeProvider>
<PostCode />
<Search />
</PostCodeProvider>
<Addresses />
</IsOpenProvider>
</AddressesProvider>
<Address />
</AddressProvider>
)
}
Let's test my app with two cases:
- Result with one address:
- Result with many addresses:
Improvement
My component worked as I expected, but it has a problem is every time I click the button search, I have to re-fetch API. It will be better if I can cache the addresses
in case the postCode
wasn't changed.
And swr
is very helpful in this case.
- I need to have a flag
wasSearched
to make sure calling API only happens when I click the button search. - When I pass
query
ofpostCode
touseSWR
,useSWR
will automatically determine the value ofpostCode
changed or not. - I have to check if
postCode
wasn't changed for two cases(one address or many addresses), and do some stuff with each case.
Let's create WasSearchedProvider
:
type WasSearchedContextType = {
wasSearched: boolean
setWasSearched: React.Dispatch<React.SetStateAction<boolean>>
}
const {Context: WasSearchedContext, useContext: useWasSearched} =
createContext<WasSearchedContextType>('WasSearched')
function WasSearchedProvider({children}: {children: React.ReactNode}) {
const [wasSearched, setWasSearched] = React.useState(false)
const value = React.useMemo(
() => ({wasSearched, setWasSearched}),
[wasSearched, setWasSearched],
)
return (
<WasSearchedContext.Provider value={value}>
{children}
</WasSearchedContext.Provider>
)
}
Change Addresses
component code to using swr
:
function AddressesProvider({children}: {children: React.ReactNode}) {
const {wasSearched} = useWasSearched()
const {postCode} = usePostCode()
const {setIsOpen} = useIsOpen()
const {address, setAddress} = useAddress()
const query = postCode.every((pc) => Boolean(pc)) ? postCode.join('-') : ''
const {data: addresses, error} = useSWR(
wasSearched ? `addresses?postCode=${query}` : null,
(arg: string) =>
fetch(arg)
.then((r) => r.json())
.then((res) => {
if (res?.data.length === 1) {
const {code, city, prefecture, ward} = res.data[0]
setAddress({
...address,
code,
city,
prefecture,
ward,
})
}
return res?.data
}),
)
useDeepCompareEffect(() => {
if (!addresses) return
if (addresses.length > 1) {
setIsOpen(true)
}
}, [{addresses}])
const value = React.useMemo(() => ({addresses, error}), [addresses, error])
return (
<AddressesContext.Provider value={value}>
{children}
</AddressesContext.Provider>
)
}
- If the response is one address, I will set the state for
address
immediately. - If the response is many addresses, I will have to wait for the
addresses
state to be set, then trigger a compare between the current state and previous state usinguseDeepCompareEffect
. If it is different, I will changeisOpen
fromfalse
->true
.
Addresses
component also need to access these states: postCode
, wasSearched
, isOpen
, setAddress
.
Let's change the providers
:
function AutoAddress() {
return (
<AddressProvider>
<WasSearchedProvider>
<PostCodeProvider>
<IsOpenProvider>
<AddressesProvider>
<PostCode />
<Search />
<Addresses />
</AddressesProvider>
</IsOpenProvider>
</PostCodeProvider>
</WasSearchedProvider>
<Address />
</AddressProvider>
)
}
Change the logic in Search
component:
function TwoArrayStringIsEqual(a: Array<string>, b: Array<string>) {
return a.every((str, idx) => str === b[idx])
}
function usePrevious<T>(value: T) {
const ref = React.useRef(value)
React.useEffect(() => {
ref.current = value
})
return ref.current
}
function Search() {
const {address, setAddress} = useAddress()
const {postCode} = usePostCode()
const previousPostCode = usePrevious<Array<string>>(postCode)
const {addresses} = useAddresses()
const {setWasSearched} = useWasSearched()
const {setIsOpen} = useIsOpen()
async function handleAddressesSearch() {
setWasSearched(true)
if (addresses && TwoArrayStringIsEqual(previousPostCode, postCode)) {
if (addresses.length === 1) {
const {code, city, prefecture, ward} = addresses[0]
setAddress({
...address,
code,
city,
prefecture,
ward,
})
} else {
setIsOpen(true)
}
}
}
return <button onClick={handleAddressesSearch}>Search</button>
}
When I change the postCode
input, It will call API, because the value of postCode
was changed. So I have to reset wasSearched
to false
.
function hanldePostCodeChange(
event: React.ChangeEvent<HTMLInputElement>,
idx: number,
) {
if (wasSearched) {
setWasSearched(false)
}
const newPostCode = [...postCode]
newPostCode.splice(idx, 1, event.target.value)
setPostCode(newPostCode)
}
Now, I can see the address list immediately and don't need to trigger a fetch request.
Change the postCode
and re-fetch API:
Conclusion
I have just built an address component
using React, and improve the performance by using swr
. Please feel free to refer to the source code.
Top comments (1)
Cool article! I would definitely use React state instead of context. Prop drilling is not so bad (and potentially non existent) for well structured components.
The only times I use context is for theme options, language options or any app wide configurations, just like the React docs mention. Cheers!