Best Practices of using TypeScript with React — Part 2
In Part 1 we explored native HTML types, Functional components, and Discriminated Unions and their pros and cons. In this part, we are going to expand our knowledge further and explore Event Handlers, Context, Hooks, and State with TypeScript.
Polymorphic Components
Make your UI component more accessible with the help of Polymorphism. Let’s consider an example of creating a Container
component for a UI library that acts as a wrapper for their child items. We need this wrapper to wrap our content in core HTML5 elements like aside
or section
.
So here is how to create a Polymorphic component that can accept a union of HTML element types as a string.
import React from "react";
type Props = React.PropsWithChildren<{
as: "div" | "section" | "aside";
}>;
function Container({ as: Component = "div", children }: Props) {
return <Component className={styles.container}>{children}</Component>;
}
The destructured alias { as: Component }
is a convention that helps illustrate that the prop is a React component and not just a string.
Our Container
now supports three different HTML elements and we can use it like so:
<Container as="section">
<p>section content</p>
</Container>
HTML Events and Forms
Now, most of these events will be inferred correctly by TypeScript, but in case you are wondering what are those, here they are.
The onClick
event inside a button
element has the type React.MouseEvent
. This defines the properties of the Synthetic event for onClick
will contain.
Forms on the other hand are a bit messier. By default the onSubmit
infers to any
, but you can explicitly define the correct type as follows:
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// ...
}
Do remember that this could change if you are using form libraries like react-hook-form
.
Typing React’s commonly used hooks
Starting with the useRef
hook, which is commonly used to reference core HTML elements. We can use TypeScript’s built-in type to pass them to the generic like so:
function TextInput() {
const inputEl = useRef<HTMLInputElement>(null);
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
Just like HTMLInputElement
, typescript has other HTML element types defined which you can use based on your needs.
Up next is useMemo
, since this returns a memoized value, the type will be inferred from the return value:
const [data, loading, error]= useApiData<Post[]>(url);
// inferred as an array of type Post from the returned value below
const filteredPosts = useMemo(() => someFilterLogic(data), [data]);
Same as above, the useCallback
hook returns a memoized function call, so the type will again be inferred from the return type:
const someDependency: CustomType = useContext(someContext);
const callbackFn = useCallback(
() => {
// some logic
return someDependency // type inferred as CustomType
},
[someDependency])
Finally, it’s the useState
hook, which could infer the type defined as the initial state, but you can always explicitly provide a type for more complex cases.
const App = () => {
const [posts, setPosts] = useState<Posts[]>([]) // type is Posts[]
const updatePosts = () => {
setPosts(null) // error: nullnot assignable to type Posts[]!
}
}
Types in Contexts and custom hooks
We are now gonna create a custom AuthContext
and learn how to create proper types for it, and also create a custom hook in the process as well.
Right off the bat, we are going the create an Interface for the type of data stored in our context.
interfact TAuthContext = {
user?: User;
token?: string;
};
const AuthContext = React.createContext<TAuthContext>({});
The createContext
hook accepts a generic to define the type of internal context data.
Up next we are gonna create a custom provider that will be used as a wrapper at the Root of our App.
interface TAuthContext = {
user?: User;
token?: string;
};
const AuthContext = React.createContext<TAuthContext>({});
interface TAuthContextProvider = {
children: React.ReactNode;
};
const AuthContextProvider = ({ children }: TAuthContextProvider) => {
const [user, setUser] = useState<User | null>();
const [token, setToken] = useState<string | null>();
useEffect(() => {
initialize();
}, []);
return (
<AuthContext.Provider value={{ user, token }}>
{children}
<AuthContext.Provider>
);
};
Finally, we will create a custom hook, that will be able to access this context from anywhere in the project.
const useAuthContext = () => React.useContext(AuthContext);
The type of this custom hook will be inferred from the AuthContext
.
And that’s a wrap. I hope this series was helpful for you guys.
A little about myself:
I am Talmeez Ahmed, Software developer at DhiWise.
DhiWise lets you build React and Flutter Apps at blazing fast speed without compromising on code-quality and developer-experience. Check out the platform to know more.