If you ever worked with react it is likely that at some point you implemented some conditional rendering. such as the following
export const Component:FC=()=>{
const [isLoading, setIsLoading] = useState<boolean>();
//
// Some logic
//
return (
<Fragment>
{ isLoading? <LoadingComponent/> : <Content/>}
</Fragment/>
)
}
While this is fine.it is limited to two values(true
& false
) which is not always the case. moreover it complicates the component code as our component grows. lets see at other way of implementing this.
interface ConditionalRendererProps{
activeState: string
mapping: { [key: string]:React.ReactNode}
}
export const ConditionalRenderer:FC<ConditionalRendererProps>=(props)=>{
return(
<Fragment>
{ props.mapping[props.activeState] }
</Fragment>
)
}
The idea behind this implementation is to use ConditionalRenderer
as a wrapper component and pass a mapping
and activeState
as a props.
mapping
as its name indicates it contains state to component mapping. meaning what component corresponds to a given state.
activeState
is selected state from the mapping props
While this could seem over complicating the implementation. it is actually more flexible that the first implementation and makes our code more cleaner. well how? okay to answer that lets take a look at another more common scenario where we need conditional rendering. when we have task that needs sometime to complete(e.g. when making API request). in this scenario there is more than two states for now to keep things simple lets agree we have four states namely initial
, processing
, complete
& error
& we want to render different components depending on the active state
export enum StateTypes{
Init='INIT',
Error='ERROR',
Success='SUCCESS',
Processing='PROCESSING',
}
The StateTypes
enum defines all possible states, next lets define generic wrapper component for components containing asynchronous actions
interface StateMachineWrapperProps{
asyncTask: Function
component: React.ComponentType<any>
}
export const StateMachineWrapper:FC<StateMachineWrapperProps> =(props) =>{
const machine = useAsync<string>(props.asyncTask)
return (
<ConditionalRenderer
activeState={machine.currentState}
mapping={{
[StateTypes.Init]:<Fragment/>,
[StateTypes.Processing]: <p>{machine.message}</p>,
[StateTypes.Error]: <p>{machine.message}</p>,
[StateTypes.Success]: <props.component {...machine.payload}/>
}}
/>
)
}
StateMachineWrapper
renders components that comprises async actions.
It is highly likely that we have multiple components that communicate to an external API or perform some other async task and for every component we can use theStateMachineWrapper
component and separate the side effect from our core component. lets see the usage...
function longRunningTask(){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve({data: 'Async task completed'})
},1000)
});
}
To keep things simple the long running task does nothing practical but it is easy to modify the implementation according to your use case. finally lets take a look at the core component...
interface ContentProps{
data: string
}
export const Content:FC<ContentProps>=(props)=>{
return (
<div>
<h3>Content</h3>
<p>{ props.data }</p>
</div>
)
}
export const ContentWrapper:FC =()=>{
return (
<StateMachineWrapper
asyncTask={longRunningTask}
component={Content}/>
)
}
the Content
component is pure component and is decoupled with the side effect(or longRunningTask
). as you can see the initial effort pays off finally because the components that mimic the StateMachineWrapper
are pure components and the concerns are separated. this is one use case to demonstrate the ease of implementing conditional rendering in such a way.
Github gist can be found here
Thank you for reading, cheers!
Top comments (1)