TypeScript for React Developers Series: How to Get Started
By adopting TypeScript, React developers take advantage of the language's type safety, code analysis, and refactoring capabilities. In this article, we'll explore the steps needed to adopt TypeScript.
Prerequisites:
- Familiar with basic TypeScript types
- Familiar with React.js
- Reading the previous article
Naming Conventions:
- use PascalCase for class, component and Enum names.
- use camelCase for variable and function names.
- do not use
I
as prefix of the interface instead useComponentNameProps
orFunctionNameParams
. - use
.generated.*
as suffix for the files are generated and you don't edit them.
interface ComponentNameProps {
position: 'left' | 'right'
}
function ComponentName({ position }: ComponentNameProps) {
if (position === 'left') {
return <div>left</div>
}
return <div>right</div>
}
interface FunctionNameParams {
args: string[]
}
function functionName({ args }: FunctionNameParams) {
return args.join(' ')
}
Types:
- put the shared types in
types.ts
file. - use generics to avoid duplicating the components/functions.
- do not export functions/types that not shared across multi-components.
- in your component file, the type definitions must comes the first.
- use Utility Types to avoid duplicating of the type definitions.
- use
readonly
to define the data coming from your API
Components:
- don't use
FunctionComponent<T>
orFC<T>
to define your component, because it's explicit about the return type, while the normal function version is implicit, read more about this codemod-replace-react-fc-typescript. - use
ReactNode
insteadJSX.Element
to define your children when use the normal functions, becauseJSX.Element
is the return value ofReact.createElement
, whileReactNode
is return value of a component - use generic components to make your component more flexible, read more about this topic.
- use the
React.ComponentPropsWithoutRef<T>
as Wrapping/Mirroring a HTML Element when you want to make component that takes all the normal props of this Element.read more about this topic
// bad
interface MyCustomLinkProps {
href: string
children?: React.ReactNode
}
function MyCustomLink(props: MyCustomLinkProps) {
const { children, href } = props
return <a href={href}>{children}</a>
}
// most recommended
function MyCustomLink(props: React.ComponentPropsWithoutRef<'a'>) {
return <a {...props}>{children}</a>
}
- use
Fragment
or use the shorthand<>
insteaddiv
in wrapping the component.
// bad
function ComponentName() {
return (
<div>
<section></section>
<section></section>
</div>
)
}
// most recommended
function ComponentName() {
return (
<>
<section></section>
<section></section>
</>
)
}
Forms and Events:
- inline event handler:
const InputComponent = () => (
<input
// TypeScript automatically inffers it
onChange={(e) => {
// logic here
}}
/>
)
- separated event handler:
const InputComponent = () => {
const onChnageInputHandler = (e: React.FormEvent<HTMLInputElement>): void => {
// your logic here
}
return <input onChange={onChnageInputHandler} />
}
or:
const InputComponent = () => {
const onChnageInputHandler: React.ChangeEventHandler<HTMLInputElement> = (
e
) => {
// your logic here
}
return <input onChange={onChnageInputHandler} />
}
onSubmit
definition for uncontrolled form component
const Form = () => (
<form
onSubmit={(e: React.SyntheticEvent) => {
e.preventDefault()
const target = e.target as typeof e.target & {
email: { value: string }
password: { value: string }
}
const email = target.email.value // typechecks!
const password = target.password.value // typechecks!
// etc...
}}
>
<label>
Email:
<input type="email" name="email" />
</label>
<label>
Password:
<input type="password" name="password" />
</label>
<button type="submit">Login</button>
</form>
)
you can use formik or any other library for handling Forms, those libraries are built on the principles of controlled components
Hooks:
useState
hook :
const Component = () => {
//1- inferred type:
// `state` is inferred to be a number
const [state, setState] = useState(0)
// `stringState` is inferred to be a string
const [stringState, setStringState] = useState('')
//2- implicit type:
const [state1, setState2] = useState<number | undefined>()
return <></>
}
useCallback
hook :
// it's just like any other function
interface MemoizedFunctionParams {
params:string[]
}
const memoizedFunction = useCallback(
(args:MemoizedFunctionParams) => {
return 'text'
},
[...],
);
useReducer
hook:
import { useReducer } from 'react'
const initialState = { count: 0 }
type ACTIONTYPE =
| { type: 'increment'; payload: number }
| { type: 'decrement'; payload: string }
function reducer(state: typeof initialState, action: ACTIONTYPE) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload }
case 'decrement':
return { count: state.count - Number(action.payload) }
default:
throw new Error()
}
}
function Counter() {
// If you don't define return type, TypeScript automatically infers it.
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>
-
</button>
<button onClick={() => dispatch({ type: 'increment', payload: 5 })}>
+
</button>
</>
)
}
useRef
hook :
import { useRef, useEffect } from 'react'
// This is just an example, React recommended you to use Controlled components
function Component() {
const inputRef = useRef<HTMLInputElement>(null)
const divRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!inputRef) {
throw Error('input not mounted !!!!')
}
inputRef.current.value = 'assign value'
}, [])
const onChnageButtonHandler = () => {
divRef.current.innerHTML = 'we change this text here'
}
return (
<div>
<input ref={inputRef} />
<div ref={divRef}>this is div content</div>
<button onClick={onChnageButtonHandler}>change the content</button>
</div>
)
}
Context:
- with default value:
import { createContext, useState, useContext } from 'react'
type ThemeContextType = 'light' | 'dark'
const ThemeContext = createContext<ThemeContextType>('light')
const App = () => {
const [theme, setTheme] = useState<ThemeContextType>('light')
return (
<ThemeContext.Provider value={theme}>
<MyComponent />
</ThemeContext.Provider>
)
}
const MyComponent = () => {
const theme = useContext(ThemeContext)
return <p>The current theme is {theme}.</p>
}
- without default value:
import { createContext, useState, useContext } from 'react'
type userContextType = { name: string; email: string }
const UserContext = createContext<userContextType>()
const App = () => {
const [user, setUser] = useState<userContextType>({
name: 'name',
email: 'email@mail.com',
})
return (
<ThemeContext.Provider value={...{ user, setUser }}>
<MyComponent />
</ThemeContext.Provider>
)
}
const MyComponent = () => {
const { user } = useContext(UserContext)
return <p>The current theme is {user?.name}.</p>
}
Conclusion:
In conclusion, using TypeScript with React can write safer, more maintainable code, and catch errors earlier in the development process. Additionally, TypeScript's static typing can help improve developer productivity by providing better tooling and documentation.