Integration with React
Integrating with React
The combination of TypeScript and React brings type safety and an improved development experience to frontend development. React's component-based philosophy naturally aligns with TypeScript's type system, and their pairing can significantly enhance code quality and maintainability.
Basic Component Type Definitions
React functional components can be defined using the React.FC
generic type or by directly annotating the props type. Class components need to extend React.Component
and specify the props and state types.
// Functional component syntax
interface ButtonProps {
text: string;
onClick?: () => void;
}
const Button: React.FC<ButtonProps> = ({ text, onClick }) => {
return <button onClick={onClick}>{text}</button>;
};
// Class component syntax
interface CounterState {
count: number;
}
class Counter extends React.Component<{}, CounterState> {
state = { count: 0 };
increment = () => {
this.setState(prev => ({ count: prev.count + 1 }));
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
Handling Event Types
React event handling requires proper annotation of event object types. React provides complete type definitions, such as React.MouseEvent
, React.ChangeEvent
, etc.
const InputField = () => {
const [value, setValue] = React.useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Submitted:', value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={handleChange}
/>
<button type="submit">Submit</button>
</form>
);
};
Typing Hooks
When using React Hooks with TypeScript, special attention should be paid to type inference. useState
can explicitly specify types via generic parameters.
interface User {
id: number;
name: string;
email: string;
}
const UserProfile = () => {
// Basic type inference
const [count, setCount] = React.useState(0);
// Object types need explicit declaration
const [user, setUser] = React.useState<User | null>(null);
// Complex state handling
const [todos, setTodos] = React.useState<Array<{id: string; text: string}>>([]);
React.useEffect(() => {
// Simulate API call
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data));
}, []);
if (!user) return <div>Loading...</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
};
Higher-Order Component Types
When creating higher-order components, proper handling of component props type passing is required. Using generics can preserve type information.
interface WithLoadingProps {
isLoading: boolean;
}
function withLoading<P extends object>(
Component: React.ComponentType<P>
): React.FC<P & WithLoadingProps> {
return ({ isLoading, ...props }: P & WithLoadingProps) => {
return isLoading ? (
<div>Loading...</div>
) : (
<Component {...props as P} />
);
};
}
// Using the HOC
interface UserListProps {
users: User[];
}
const UserList: React.FC<UserListProps> = ({ users }) => (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
const UserListWithLoading = withLoading(UserList);
// Using the enhanced component
const App = () => {
const [loading, setLoading] = React.useState(true);
const [users, setUsers] = React.useState<User[]>([]);
React.useEffect(() => {
setTimeout(() => {
setUsers([{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}]);
setLoading(false);
}, 1000);
}, []);
return <UserListWithLoading isLoading={loading} users={users} />;
};
Type-Safe Context API
When using Context, creating a typed createContext can prevent runtime errors.
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
// Provide default value and specify type
const ThemeContext = React.createContext<ThemeContextType>({
theme: 'light',
toggleTheme: () => {},
});
const ThemeProvider: React.FC = ({ children }) => {
const [theme, setTheme] = React.useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const ThemedButton = () => {
// Consuming Context provides correct type hints
const { theme, toggleTheme } = React.useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff',
}}
>
Toggle Theme
</button>
);
};
Handling Third-Party Libraries
When using third-party React libraries, you may need to extend their type definitions or create type declarations.
// Assuming we're using a third-party library without types
declare module 'untyped-library' {
interface UntypedComponentProps {
size?: 'sm' | 'md' | 'lg';
variant?: 'primary' | 'secondary';
onClick?: () => void;
}
export const UntypedComponent: React.FC<UntypedComponentProps>;
}
// Using the extended types
import { UntypedComponent } from 'untyped-library';
const ThirdPartyUsage = () => {
return (
<UntypedComponent
size="md"
variant="primary"
onClick={() => console.log('Clicked')}
/>
);
};
Performance Optimization and Types
When using optimization techniques like React.memo
and useCallback
, the type system can help verify whether props have changed.
interface ExpensiveComponentProps {
items: string[];
onSelect: (item: string) => void;
}
const ExpensiveComponent = React.memo<ExpensiveComponentProps>(
({ items, onSelect }) => {
console.log('Component rendered');
return (
<ul>
{items.map(item => (
<li key={item} onClick={() => onSelect(item)}>
{item}
</li>
))}
</ul>
);
},
(prevProps, nextProps) => {
// Custom comparison function, returns true if no re-render is needed
return (
prevProps.items.length === nextProps.items.length &&
prevProps.items.every((item, i) => item === nextProps.items[i])
);
}
);
const ParentComponent = () => {
const [items] = React.useState(['Apple', 'Banana', 'Orange']);
const [selected, setSelected] = React.useState<string | null>(null);
// Use useCallback to avoid creating new functions on every render
const handleSelect = React.useCallback((item: string) => {
setSelected(item);
}, []);
return (
<div>
<p>Selected: {selected || 'None'}</p>
<ExpensiveComponent items={items} onSelect={handleSelect} />
</div>
);
};
Form Handling and Types
In complex form scenarios, the type system can ensure the structural consistency of form data.
interface FormValues {
username: string;
password: string;
remember: boolean;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
const ComplexForm = () => {
const [values, setValues] = React.useState<FormValues>({
username: '',
password: '',
remember: false,
preferences: {
theme: 'light',
notifications: true,
},
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setValues(prev => {
if (name.startsWith('preferences.')) {
const prefKey = name.split('.')[1] as keyof FormValues['preferences'];
return {
...prev,
preferences: {
...prev.preferences,
[prefKey]: type === 'checkbox' ? checked : value,
},
};
}
return {
...prev,
[name]: type === 'checkbox' ? checked : value,
};
});
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Form submitted:', values);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Username:
<input
type="text"
name="username"
value={values.username}
onChange={handleChange}
/>
</label>
</div>
<div>
<label>
Password:
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
/>
</label>
</div>
<div>
<label>
<input
type="checkbox"
name="remember"
checked={values.remember}
onChange={handleChange}
/>
Remember me
</label>
</div>
<fieldset>
<legend>Preferences</legend>
<div>
<label>
Theme:
<select
name="preferences.theme"
value={values.preferences.theme}
onChange={handleChange}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
</div>
<div>
<label>
<input
type="checkbox"
name="preferences.notifications"
checked={values.preferences.notifications}
onChange={handleChange}
/>
Enable notifications
</label>
</div>
</fieldset>
<button type="submit">Submit</button>
</form>
);
};
Routing Integration
When integrating with routing libraries like React Router, the type system can enhance the safety of route parameters.
import { BrowserRouter as Router, Route, Link, useParams } from 'react-router-dom';
interface ProductParams {
id: string;
}
const ProductPage = () => {
// The useParams hook can now return parameters of a specific type
const { id } = useParams<ProductParams>();
const [product, setProduct] = React.useState<{ name: string; price: number }>();
React.useEffect(() => {
// Fetch product data based on id
fetch(`/api/products/${id}`)
.then(res => res.json())
.then(data => setProduct(data));
}, [id]);
if (!product) return <div>Loading...</div>;
return (
<div>
<h2>{product.name}</h2>
<p>Price: ${product.price}</p>
</div>
);
};
const AppWithRouter = () => {
return (
<Router>
<nav>
<ul>
<li><Link to="/product/1">Product 1</Link></li>
<li><Link to="/product/2">Product 2</Link></li>
</ul>
</nav>
<Route path="/product/:id">
<ProductPage />
</Route>
</Router>
);
};
State Management Integration
When integrating with state management libraries like Redux or MobX, the type system can ensure consistency between actions and state.
// Redux example
import { createStore, applyMiddleware } from 'redux';
import { useSelector, useDispatch } from 'react-redux';
import thunk, { ThunkAction } from 'redux-thunk';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface State {
todos: Todo[];
loading: boolean;
}
// Define action types
type Action =
| { type: 'ADD_TODO'; payload: string }
| { type: 'TOGGLE_TODO'; payload: number }
| { type: 'FETCH_TODOS_REQUEST' }
| { type: 'FETCH_TODOS_SUCCESS'; payload: Todo[] };
// Define thunk action type
type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
State,
unknown,
Action
>;
// Reducer function
const reducer = (state: State = { todos: [], loading: false }, action: Action): State => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: Date.now(),
text: action.payload,
completed: false,
},
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
),
};
case 'FETCH_TODOS_REQUEST':
return { ...state, loading: true };
case 'FETCH_TODOS_SUCCESS':
return { ...state, loading: false, todos: action.payload };
default:
return state;
}
};
// Create store
const store = createStore(reducer, applyMiddleware(thunk));
// Action creators
const addTodo = (text: string): Action => ({
type: 'ADD_TODO',
payload: text,
});
const fetchTodos = (): AppThunk => dispatch => {
dispatch({ type: 'FETCH_TODOS_REQUEST' });
fetch('/api/todos')
.then(res => res.json())
.then(todos => {
dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: todos });
});
};
// Usage in React component
const TodoApp = () => {
const todos = useSelector((state: State) => state.todos);
const loading = useSelector((state: State) => state.loading);
const dispatch = useDispatch();
const [text, setText] = React.useState('');
React.useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (text.trim()) {
dispatch(addTodo(text));
setText('');
}
};
if (loading) return <div>Loading todos...</div>;
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={e => setText(e.target.value)}
/>
<button type="submit">Add Todo</button>
</form>
<ul>
{todos.map(todo => (
<li
key={todo.id}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
>
{todo.text}
</li>
))}
</ul>
</div>
);
};
// Provide store at the app root
const ReduxApp = () => (
<Provider store={store}>
<TodoApp />
</Provider>
);
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn