https://github.com/david-gmz/taskappts
Project task made on ReactJS 18 with TS
https://github.com/david-gmz/taskappts
context-api learning-by-doing reactjs reducer tailwind-css typescript usecallback-hook usememo-hook
Last synced: about 1 month ago
JSON representation
Project task made on ReactJS 18 with TS
- Host: GitHub
- URL: https://github.com/david-gmz/taskappts
- Owner: david-gmz
- Created: 2024-11-15T19:33:15.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2024-11-30T12:47:57.000Z (about 1 year ago)
- Last Synced: 2025-03-11T05:47:05.417Z (11 months ago)
- Topics: context-api, learning-by-doing, reactjs, reducer, tailwind-css, typescript, usecallback-hook, usememo-hook
- Language: TypeScript
- Homepage:
- Size: 541 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# React TS Task Project
(TypeScript Journey Part II)
## Intro
The main purpose of this project based on one of the lessons in the course "React - The Complete Guide"[^1] *by Maximilian Schwarzmuller* is to practice TypeScript. In addition *Context API*, *useReducer* and *Redux*, three popular ways of state managment.
Define `{...props}` type
## How to define the spread property in a component
### Narrowing came to the rescue
I spent quite a lot of time trying to solve this doubt on how to type the spread property of a component what is shown in the next snippet:
---
```ts
interface InputProps {
isTextarea: boolean,
label: string,
props: // Which type is this?
}
export default function input({ isTextarea, label, ...props }: InputProps)
{
return (
{label}
{isTextarea ? : }
)
}
```
For the props field in the InputProps interface, we want to account for the different props accepted by `` and ``. Since textarea and input elements share many props but also have unique ones, we can use TypeScript's built-in utility types.
## Solution
We can use a discriminated union to conditionally handle the props depending on the isTextarea flag. Here's how:
```ts
import React from "react";
interface InputPropsBase {
label: string;
}
interface InputPropsTextArea extends InputPropsBase {
isTextarea: true;
props?: React.TextareaHTMLAttributes;
}
interface InputPropsInput extends InputPropsBase {
isTextarea: false;
props?: React.InputHTMLAttributes;
}
type InputProps = InputPropsTextArea | InputPropsInput;
```
## Explanation
1. **Base Properties:**
* The label property is common to both cases, so it's extracted into a base interface InputPropsBase.
2. **Conditional Props:**
* *InputPropsTextArea:* Includes isTextarea: true and allows `React.TextareaHTMLAttributes as props`.
* *InputPropsInput:* Includes isTextarea: false and allows `React.InputHTMLAttributes as props`.
3. **Discriminated Union:**
* Using `isTextarea` as the discriminator ensures that TypeScript will enforce the correct props type based on its value.
4. **Default Props:**
* Added `props = {}` to avoid undefined props when spreading.
However the spread operator `(...props)` does not automatically narrow the type of props to either `React.TextareaHTMLAttributes` or `React.InputHTMLAttributes` based on `isTextarea`. Like:
```ts
export default function Input({ isTextarea, label, props = {} }: InputProps) {
return (
{label}
{isTextarea ? (
//This going to cause a mismatch
) : (
//This going to cause a mismatch
)}
);
}
```
It's going to attempt to assign the full union of both types to each element, causing a mismatch for event handlers like `onChange`.
We need to narrow the type explicitly before spreading props.
### Narrow Props Based on isTextarea
```ts
export default function Input({ isTextarea, label, props = {} }: InputProps) {
if (isTextarea) {
// Narrow to TextArea props
const textareaProps = props as React.TextareaHTMLAttributes;
return (
{label}
);
} else {
// Narrow to Input props
const inputProps = props as React.InputHTMLAttributes;
return (
{label}
);
}
}
```
## Explanation
1. **Explicit Type Narrowing:**
Before spreading props, explicitly cast props to the correct type (`TextareaHTMLAttributes` or `InputHTMLAttributes`) using a `const` assignment.This ensures TypeScript knows the exact type of props when spreading into the respective element.
2. **Union Resolution:**
The conditional `if (isTextarea)` ensures TypeScript understands which branch is active, allowing us to safely narrow props.
3. **Safe Spreading:**
After narrowing, spreading `textareaProps` or `inputProps` will no longer throw type errors, as their types align perfectly with the attributes of `` and `` respectively.
***TypeScript's type narrowing** requires clear distinctions in code flow, and unions don’t automatically propagate to props when destructuring. By explicitly casting and separating the logic, we ensure correctness.*
More on spread props
## Using `onClick` in a ``
```ts
import React from "react";
interface ButtonProps extends React.ButtonHTMLAttributes {
label: string;
}
export default function Button({ label, ...props }: ButtonProps) {
return (
{label}
);
}
```
### Explanation:
By extending `React.ButtonHTMLAttributes`, the Button component automatically supports all valid attributes of a ``, such as onClick, disabled, type, etc.
### TypeScript Validation
1. **TypeScript ensures that:**
- `onClick` is properly typed as
`(event: React.MouseEvent) => void.`
- Other invalid attributes are caught. For example, passing an invalid attribute like rows to a `` would result in an error:
```ts
// ❌ Error: 'rows' does not exist on type 'ButtonHTMLAttributes'
```
### Key Takeaways
- onClick is an intrinsic attribute of ``, and we don’t need to define it explicitly in our interface when extending `React.ButtonHTMLAttributes`.
- Using TypeScript’s intrinsic attributes for HTML elements ensures our props are aligned with the standard DOM attributes.
### Why Use label Instead of children?
1. **Semantic Clarity:**
- label explicitly communicates that the string is the button's text content.
- children is more generic and implies flexibility (e.g., the ability to nest other components).
2. **Consistency:**
- If your component has other structured props (like icon, variant, etc.), using label keeps the API clear and avoids ambiguity:
```ts
} variant="primary" />;
```
3. **Flexibility for Other Features:**
- If we later decide to allow additional customizations (like an optional icon or aria-label for accessibility), having a dedicated label makes it easier to manage:
```ts
interface ButtonProps extends React.ButtonHTMLAttributes {
label: string; // Text shown on the button
icon?: React.ReactNode; // Optional icon to display
}
} />;
```
### Comparison
Using children:
```ts
alert("Clicked!")}>Click Me;
```
Using label:
```ts
alert("Clicked!")} label="Click Me" />;
```
Both work, but the second option (label) is more explicit for text-only buttons.
Typing forwardRef in React
## Properly type our React.forwardRef function for Input
### handle the ref argument
So starting with this part of the Input component:
```ts
const Input = React.forwardRef(function Input({ isTextarea, label, props = {} }: InputProps, ref) {
//code...
//more code...
})
```
To properly type our `React.forwardRef` function for `Input`, we need to handle the ref argument and its typing. Since ref will either point to a textarea or an input element based on the `isTextarea` prop, you'll need to define a generic type that accommodates both.
Here’s the updated and typed `React.forwardRef` implementation:
```ts
// models.read-the-docs
interface InputPropsBase {
label: string;
}
interface InputPropsTextArea extends InputPropsBase {
isTextarea: true;
props?: React.TextareaHTMLAttributes;
}
interface InputPropsInput extends InputPropsBase {
isTextarea: false;
props?: React.InputHTMLAttributes;
}
type InputProps = InputPropsTextArea | InputPropsInput;
// Input.tsx
const Input = React.forwardRef<
HTMLTextAreaElement | HTMLInputElement,
InputProps
>(function Input({ isTextarea, label, props = {} }: InputProps, ref) {
//code...
}
className={classesInput}
{...textareaProps}
/>
//more code...
}
className={classesInput}
{...inputProps}
/>
})
```
### Key Changes and Explanation:
1. **ForwardRef Type:**
- The `React.forwardRef` generic type is defined as ``.
- This ensures the ref can point to either an HTMLTextAreaElement or an `HTMLInputElement`, based on `isTextarea`.
2. **Casting ref:**
- Inside the conditional branches, the ref is cast to the appropriate type using `React.Ref` or `React.Ref`.
3. **Fallback for props:**
- The props property in InputProps is still optional and defaults to an empty object ({}).
### Usage:
When consuming the Input component with a ref, TypeScript will correctly infer the type based on the isTextarea prop:
```ts
// NewProject.tsx
import React from "react";
import Input from "./Input";
export default function NewProject() {
constefining Dtype = in Modal component React.useRef(null);
const description = React.useRef(null);
const dueDate = React.useRef(null);
return (
// code component
//more code...
)
}
```
specific type of the dialog ref
## Defining type in Modal component
```ts
//Modal.tsx
//code of the component
open() {
if (dialog.current !== undefined) dialog.current.showModal(); // ❌Error: Property 'showModal' does not exist on type 'never'.ts(2339)
}
//some more code..
{children}, // ❌Error: Type 'MutableRefObject' is not assignable to type 'LegacyRef | undefined'. Type 'MutableRefObject' is not assignable to type 'RefObject'. Types of property 'current' are incompatible. Type 'undefined' is not assignable to type 'HTMLDialogElement | null'.ts(2322)
```
### Solution
By default, `React.useRef()` is initialized with undefined, leading TypeScript to infer the type as `MutableRefObject`. Since we're working with an HTML `` element, we should provide the correct type for the dialog `ref: HTMLDialogElement | null`.
Here's a corrected version of your Modal component:
```ts
// Ensure the modal root exists
const modalRoot = document.getElementById("modal-root");
const Modal = forwardRef(function Modal(
{ children }: ModalProps,
ref: React.Ref<{ open: () => void }>
) {
// Define the ref with the correct type
const dialog = useRef(null);
// Use imperative handle to expose functions to the parent component
useImperativeHandle(ref, () => ({
open() {
if (dialog.current) {
dialog.current.showModal();
}
},
}));
// Render the dialog inside the modal root using portals
if (modalRoot) {
return createPortal(
{children},
modalRoot
);
}
return null;
});
```
## Explanation of Fixes:
1. **Type for dialog Ref:**
- Changed `const dialog = useRef();` to `const dialog = useRef(null);` to specify that the dialog ref references an HTML `` element.
2. **useImperativeHandle Type:**
- Defined the type of `ref` as `React.Ref<{ open: () => void }>` to specify that the parent component can use the open function.
3. **Portal Check:**
- Added a check for modalRoot to handle cases where modal-root is missing, ensuring a graceful fallback.
4. **createPortal Typing:**
- Fixed dialog ref type mismatch by ensuring it matches the expected type `React.RefObject`.
*This should eliminate the TypeScript errors and ensure proper type safety in your component.*
## Why ref as React.Ref<{ open: () => void }>?
1. **Default Behavior of ref:**
- Normally, a ref in React points directly to an element (`HTMLDialogElement`, `HTMLDivElement`, etc.). However, when you use `forwardRef` with imperative handles, you're essentially customizing what the parent can "see" through that ref.
2. **Custom API for the Parent:**
- Instead of exposing the raw DOM node (HTMLDialogElement), you're exposing an object with specific methods, like `{ open: () => void }`. TypeScript requires we to explicitly define the shape of that object.
3. **React's Ref Type:**
The type `React.Ref` represents a ref that can either:
- Be a callback ref (function).
- Be a `RefObject` (created by useRef).
- Or be null.
Since our component will expose the open() method, we declare the ref type as `React.Ref<{ open: () => void }>`, letting TypeScript know exactly what the parent will receive.
fix the typing issue in our handleAddTask function.
## Match the TaskProps
```ts
// App.tsx
const handleAddTask = (text: string) => {
setStateProjects((prevStateProjects: InitState) => {
if (!prevStateProjects.selectedProjectId) {
return prevStateProjects; // Return unchanged if no project is selected
}
const newTask: TaskProps = {
id: prevStateProjects.selectedProjectId,
text,
taskId: Date.now()
};
return {
...prevStateProjects,
tasks: [newTask, ...prevStateProjects.tasks]
};
});
};
```
That was a first solution. Then I added ainterface TasksProps {tasks: Task[]}
Refactoring with Context API redefining types
## Advantages of Context API with TypeScript
1. **Avoids Prop Drilling**
- Context simplifies the process of passing data deeply nested in a component tree. This is especially useful for global states like themes, authentication, or language preferences.
2. **Improved Type Safety**
- TypeScript ensures that the context's shape is consistent across components. With well-defined types, developers are less prone to runtime errors caused by mismatched data structures.
3. **Better Developer Experience**
- Intellisense in IDEs (e.g., VSCode) leverages TypeScript types, making it easier to use context values correctly and reducing the learning curve for new developers.
4. **Scalability for Small to Medium Apps**
- Context API works well for apps with manageable state requirements, providing a simpler alternative to libraries like Redux for medium-sized projects.
---
[^1]: This course can be found in Udemy website.