notion-image

Hey everyone! I'm Samet, a fullstack web developer who specializes in back-of-the-frontend code, particularly with React.js. Today, I'll teach you useful code hacks to supercharge your codebase.

What you'll learn

  • The flaws of the context API
  • The logical wrapper factory
  • The action factory

Before We Begin

I want to introduce my common way of writing context providers, so that I can write custom components and hooks and the you‘ll understand their meaning without me having to explain too much. If you want me to do a more simple context tutorial in the future, let me know in the comments.

const ContextA = React.createContext();

export function useContextA() {
	return useContext(ContextA)
}

export default function ContextAProvider({ children }) {
	// some state logic
	return (
		<ContextA.Provider value={...}>
			{children}
		<ContextA.Provider>
	)
}

The Flaws of the context API

The Context API is the state-of-the-art way to manage state in React. It is an alternative to Redux, MobX, and a large variety of state management libraries made by the community.

Repetitive Tasks

Almost every time I use the Context API in a frontend application, I run into doing the exact same thing: I create an AuthContext, like the one below:

//AuthContext.js
const AuthContext = React.createContext();
export function useAuth() {
	return useContext(AuthContext);
}

const AuthContextProvider = ({ children }) => {
	const [isSignedIn, setSignedIn] = useState();
	//some more state...
	return (
		<AuthContext.Provider value={{ isSignedIn, ...}}>
			{children}
		</AuthContext.Provider>
	)
}

Then, I use the signed in state anywhere in my app:

//Some Component
const { isSignedIn } = useAuth();
return isSignedIn ? (
	<AuthenticatedComponent />
) : (
	<NotAuthenticatedComponent />
)

Imagine having 60 auth-protected component in your app. Your codebase would be a mess!

What are the problems with this approach?

  • Messy code
  • Repeating a lot of code all the time

Easy to mess up your codebase

Suppose you have a context and a ContextProvider component attached to it.

In the same component you provided the context, you want to use the context for a button, like the example below:

// ComponentA.js
function ComponentA() {
	return (
		<ContextProvider>
			<Context.Consumer>
				(context) => (
					<button onClick={context.doSomething}>
						Do Something
					</button>
				)
			</Context.Consumer>
		</ContextProvider>
	)
}

You wanted to do something really simple, but ended up messing up your codebase. As a workaround, you might do something like this:

// ComponentA.js
function ComponentA() {
	return (
		<ContextProvider>
			<ComponentB />
		</ContextProvider>
	)
}

function ComponentB() {
	const { doSomething } = useComponentAContext();
	return <button onClick={doSomething}>Do Something</button>
}

But now, we are back in the repetitive tasks zone.

Component Factories to the rescue

A component factory is a function that returns a React functional component.I. e: () => () => <div></div>

We can use component factories to clean up our codebase and create a clean code structure. Today, I will teach you about 2 time-saving and life-saving factories: The context logical wrapper and the context action factory.

Logical Wrappers

Logical Wrappers solve the problems of conditional rendering we saw in the beginning, without having to mess up your codebase. You create them with this function:

export function createLogicalWrapper(context, consumer) {
	return function LogicalWrapper ({ children }) {
		const ctx = useContext(context);
		const condition = useMemo(() => consumer(ctx), [ctx]);
		return <>{Condition ? children : null}
	}
}

Then, our problem from before can be solved in just a few lines of code:

// AuthContext.js
const AuthContext = React.createContext();

export const Authenticated = createLogicalWrapper(AuthContext, ctx => ctx.isSignedIn);
export const NotAuthenticated = createLogicalWrapper(AuthContext, ctx => !ctx.isSignedIn);

Provider, etc...

// AnyComponent.js
export default SuperProtectedComponent () {
	return <Authenticated>Hello, signed in user</Authenticated>
}

You can save tons of time by using this component factory for every context you create.

Context actions

Context actions are high-reusable components because of the way they are built.

export function createAction(label, context, consumer) {
	return function ActionButton({ className }) {
		const ctx = useContext(context);
		const clickHandler = useCallback(() => consumer(ctx), [ctx]);
		return <button onClick={clickHandler} className={className}>{label}</button>
	}
}

Now, we can create out ComponentB with just one line of code!

// ComponentA.js
const DoSomethingButton = createAction(Context, ctx => ctx.doSomething());

function ComponentA () {
	return (
		<ContextProvider>
			<DoSomethingButton />
		</ContextProvider>
	)
}

Now you must ask yourselves - why would you add the className prop to your context action? The reason is simple: Reusability. The best way to demonstrate it is by example:

const LogoutButton = createAction(AuthContext, ctx => ctx.logout());

// Navbar.js
...
<Authenticated>
	<LogoutButton className="nav-link" />
</Authenticated>

// Profile.js
<Authenticated>
	<div>Signed in as {user.name}</div>
	<LogoutButton className="button"
</Authenticated>