Quick start (v2)
Building Your To-Do App: A Beginner’s Journey with Qbit Components - 2
This tutorial guides you through creating a simple TODO application using React, TypeScript, TailwindCSS, and the Qbit Design library. By the end of this guide, you’ll have a clear understanding of key concepts such as components, state management, and Qbit skins.
Step 1: Create a React Application with TypeScript
Before anything else, you’ll need to have Node.js installed. This is the tool that allows us to run React (which is the framework we'll use to build our app). You can download Node.js from here.
Once that’s done, let’s create our project folder. Open your terminal (or command prompt) and run:
npx create-react-app todo-app --template typescript
cd todo-app
This command creates a new React project named todo-app
with TypeScript support. The --template typescript
ensures the project uses TypeScript.
Step 2: Install TailwindCSS
To make our app beautiful, we’ll be using Tailwind CSS, a utility-first CSS framework. This will help style our app quickly without needing to write custom CSS. Follow these steps to install it:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
Then, go into the tailwind.config.js
file and update it with:
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
Next, open the src/index.css
file and replace its contents with:
@tailwind base;
@tailwind components;
@tailwind utilities;
After that, your project is ready to style with Tailwind.
Run the project:
npm start
Output
Step 3: Project Folder Structure
Organize your project as follows:
src/
todo/
interface/
ITodo.ts
skins/
Button.skin.todo.close.tsx
Button.skin.todo.tsx
Card.skin.todo.tsx
Modal.skin.todo.tsx
TextInput.skin.todo.tsx
TodoForm.tsx
TodoList.tsx
App.tsx
index.css
Step 4: Modify App.tsx
const App = () => {
return (
<div className="max-w-4xl mx-auto mt-12">
<h2 className="text-3xl font-bold text-center">TODO</h2>
</div>
);
};
export default App;
- This creates the main layout for the TODO application.
- The
max-w-4xl
centers the content and sets a maximum width.
Output
Step 5: Create TodoForm.tsx
and TodoList.tsx
src/todo/TodoForm.tsx
const TodoForm = () => {
return (
<form className="mt-8">
{/* */}
</form>
);
};
export default TodoForm;
src/todo/TodoList.tsx
const TodoList = () => {
return (
<div>
{/* */}
</div>
);
};
export default TodoList;
Step 6: Integrate Components into App.tsx
import TodoList from './todo/TodoList';
import TodoForm from './todo/TodoForm';
const App = () => {
return (
<div className="max-w-4xl mx-auto mt-12">
<h2 className="text-3xl font-bold text-center">TODO</h2>
<TodoForm />
<TodoList />
</div>
);
};
export default App;
- This integrates the form for adding TODOs and the list for displaying them.
Step 7: Enhance TodoForm
Add Form Submit Function:
const TodoForm = () => {
const handleAddTodo = () => {};
return (
<form className="mt-8" onSubmit={handleAddTodo}>
{/* */}
</form>
);
};
export default TodoForm;
- The `handleAddTodo` function is necessary for intercepting the form submission event and handling custom logic, such as adding a new TODO item.
Handle Submit:
const handleAddTodo = (e: React.FormEvent) => {
e.preventDefault();
};
- Adding `preventDefault` ensures the page does not reload when the form is submitted, allowing us to handle the logic entirely within React.
Step 8: Add Input Field Using Qbit TextInput Component into TextForm
import React, { useState } from 'react';
import { TextInput } from '@components/inputs';
import TodoSkinTextInput from './skins/TextInput.skin.todo';
const TodoForm = () => {
const [task, setTask] = useState<string>('');
const handleAddTodo = (e: React.FormEvent) => {
e.preventDefault();
};
return (
<form className="mt-8" onSubmit={handleAddTodo}>
<label htmlFor="todo" className="text-sm/6 font-medium text-gray-900">
Todo
</label>
<TextInput
type="text"
name="todo"
id="todo"
autoComplete="off"
placeholder="Enter your todo"
required
value={task}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTask(e.target.value)}
/>
</form>
);
};
export default TodoForm;
Explanation of const [task, setTask] = useState<string>('');
This line creates a state variable in React to store and update the user's input.
How It Works:
task
: Holds the current value of the state. Initially, it's an empty string (''
).setTask
: A function used to update thetask
state when the user types in the input field.useState<string>
: A React Hook that manages state in functional components. The<string>
type ensures thetask
variable only holds string values.
Explanation of value
and onChange
in TextInput
value
Property:
The value
property binds the input field's content to the task
state. This means:
- The input field always displays the current value of
task
. - If
task
is updated, the input field automatically reflects the new value.
onChange
Event Handler:
The onChange
property listens for changes in the input field and updates the task
state accordingly.
How It Works:
- When the user types into the input field, the
onChange
function is triggered. setTask(e.target.value)
updates the state with the user's input (e.target.value
).
Combined Functionality:
By using value
and onChange
together:
- The input field is always synchronized with the
task
state. - The app can react to user input in real time.
value={task}
ensures the input field displays the current state.onChange
updates the state as the user types.
By understanding these properties, you’ll be able to handle user inputs dynamically in your React application.
If you want more details about managing state in React, visit React's State Management.
You should pass the renderer. If it's not passed it will throw an error like No Renderer Specified
Output
- You should pass the renderer. If it's not passed it will throw an error like No Renderer Specified. So you need to pass the skin into the renderer prop.
- You can import the pre-designed skins from Qbit Designs. you can check the official documentation to know about the skins. OR you can design your customized skin and pass it into the renderer prop.
Step 9: Create Custom Skin For TextInput
To resolve the error, create a custom skin for the TextInput component.
src/todo/skins/TextInput.skin.todo.tsx
const TodoSkinTextInput: com.qbit.Skin<TextInputProps> = () => {
return (
<div>
{/* */}
</div>
);
};
export default TodoSkinTextInput;
Update TodoSkinTextInput
:
const TodoSkinTextInput: com.qbit.Skin<TextInputProps> = (props: com.qbit.SkinProps<TextInputProps>) => {
const {
name,
id,
type,
value,
placeholder,
required,
disabled,
maxLength,
autoComplete,
onChange,
} = props;
return (
<input
id={id}
name={name}
type={type}
value={value}
placeholder={placeholder}
required={required}
disabled={disabled}
maxLength={maxLength}
autoComplete={autoComplete}
className="block w-full rounded-md bg-white px-3 py-2 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-[#212121] sm:text-sm/6"
onChange={(event) => onChange?.(event)}
/>
);
};
export default TodoSkinTextInput;
For instance, if you're creating a skin for a TextInput, you can use TextInputProps
as shown below:
const TodoSkinTextInput: com.qbit.Skin<TextInputProps> = (props: com.qbit.SkinProps<TextInputProps>) => {
- The component uses the TypeScript generic type
com.qbit.Skin<TextInputProps>
, which enforces that the props (TextInputProps
) adhere to the provided type definition. TextInputProps
defines the properties that can be passed to this component. (The exact definition ofTextInputProps
is not included here, but its usage is demonstrated below.)
Destructuring Props
You can destructure the props for easier access:
const { name, id, type, value, placeholder, required, disabled, maxLength, autoComplete, onChange } = props;
Props Explanation
name
: The name of the input field.id
: A unique identifier for the input field.type
: Specifies the type of input (e.g.,text
,password
, etc.).value
: The current value of the input field.placeholder
: Placeholder text displayed when the input field is empty.required
: Boolean indicating whether the field is required.disabled
: Boolean indicating whether the field is disabled.autoComplete
: Specifies whether the input should have autocomplete functionality.onChange
: Event handler for changes to the input's value.
Here we are using required props for the skin. You can check for the official documentation for other props. Check here
Styling
The component uses Tailwind CSS for styling.
- Base Styles:
block w-full rounded-md bg-white px-3 py-2 text-base text-gray-900
creates a full-width, rounded input with padding and default text styles.
- Outline Behavior:
outline outline-1 -outline-offset-1 outline-gray-300
adds a subtle outline for better visibility.- Focus styles (
focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-[#212121]
) highlight the input when it's focused.
- Placeholder Styling:
placeholder:text-gray-400
ensures the placeholder text is subtly visible.
- Responsive Text Size:
sm:text-sm/6
adjusts text size for smaller screens.
Event Handling
The onChange
handler listens for input changes and updates the value accordingly. Optional chaining ensures no error occurs if onChange
is undefined.
Update TodoForm
:
Modify the TextInput component in TodoForm
to use the custom renderer.
import TodoSkinTextInput from './skins/TextInput.skin.todo';
<TextInput
type="text"
name="todo"
id="todo"
placeholder="Enter your todo"
required
value={task}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTask(e.target.value)}
renderers={{ renderer: TodoSkinTextInput }}
/>
Output
Step 10: Add Button Using Qbit Button Component into TextForm
import React, { useState } from 'react';
import { Button, TextInput } from '@components/inputs';
import TodoSkinTextInput from './skins/TextInput.skin.todo';
const TodoForm = () => {
const [task, setTask] = useState<string>('');
const handleAddTodo = (e: React.FormEvent) => {
e.preventDefault();
};
return (
<form className="mt-8" onSubmit={handleAddTodo}>
<label htmlFor="todo" className="text-sm/6 font-medium text-gray-900">
Todo
</label>
<TextInput
type="text"
name="todo"
id="todo"
autoComplete="off"
placeholder="Enter your todo"
required
value={task}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTask(e.target.value)}
renderers={{ renderer: TodoSkinTextInput }}
/>
<div className="mt-3">
<Button value="Add Todo" onClick={() => {}} />
</div>
</form>
);
};
export default TodoForm;
You should pass the renderer. If it's not passed it will throw an error like No Renderer Specified
Output
- You should pass the renderer. If it's not passed it will throw an error like No Renderer Specified. So you need to pass the skin into the renderer prop.
- You can import the pre-designed skins from Qbit Designs. you can check the official documentation to know about the skins. OR you can design your customized skin and pass it into the renderer prop.
Step 11: Create Custom Skin for Button
To resolve the error, create a custom skin for the Button component.
src/todo/skins/Button.skin.todo.tsx
const TodoSkinButton: com.qbit.Skin<ButtonProps> = () => {
return (
<div>
{/* */}
</div>
);
};
export default TodoSkinButton;
Update TodoSkinButton
:
const TodoSkinButton: com.qbit.Skin<ButtonProps> = () => {
const { value, onClick, disabled } = props;
return (
<button
disabled={disabled}
onClick={onClick}
className="flex w-full justify-center rounded-md bg-[#212121] px-3 py-2 text-sm/6 font-semibold text-white shadow-sm hover:bg-neutral-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#212121]"
>
{value}
</button>
);
};
export default TodoSkinButton;
For instance, if you're creating a skin for a Button, you can use ButtonProps
as shown below:
const TodoSkinButton: com.qbit.Skin<ButtonProps> = (props: com.qbit.SkinProps<ButtonProps>) => {
- The component uses the TypeScript generic type
com.qbit.Skin<ButtonProps>
, which enforces that the props (ButtonProps
) adhere to the provided type definition. ButtonProps
defines the properties that can be passed to this component. (The exact definition ofButtonProps
is not included here, but its usage is demonstrated below.)
Destructuring Props
You can destructure the props for easier access:
const { value, onClick, disabled } = props;
Props Explanation
value
: Text displayed on the button.onClick
: Event handler triggered when the button is clicked.disabled
: Boolean to disable the button when necessary.
Here we are using required props for the skin. You can check for the official documentation for other props. Check here
Styling
The component uses Tailwind CSS for styling.
- Base Styles:
flex w-full justify-center rounded-md bg-[#212121] px-3 py-2 text-sm/6 font-semibold text-white
- Creates a full-width button with a dark background, centered text, and rounded corners.
- Hover Effect:
hover:bg-neutral-900
- Changes the background to a slightly lighter shade when hovered.
- Focus Styles:
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#212121]
- Adds a visible outline for accessibility when the button is focused.
Event Handling
The onClick
handler listens for click events and executes the specified function. Optional chaining ensures no error occurs if onClick
is undefined.
Update TodoForm
:
Modify the Button component in TodoForm
to use the custom renderer.
import TodoSkinButton from './skins/Button.skin.todo';
<Button
renderers={{ renderer: TodoSkinButton }}
value="Add Todo"
onClick={() => {}}
/>
Output
Step 12: Define the ITodo
Interface and Initialize State in App
In this step, you’ll create an interface to structure TODO items and use it to define the state in the App
component.
src/todo/interface/ITodo.ts
Create an interface for TODO items to enforce a consistent structure.
export interface ITodo {}
Update ITodo
export interface ITodo {
date: Date;
task: string;
}
date
: ADate
object representing when the TODO was created.task
: Astring
containing the TODO description.
Step 13: Initialize State in App
Modify the App
component to include state for a single TODO item.
Updated App.tsx
import { useState } from 'react';
import TodoForm from './todo/TodoForm';
import { ITodo } from './todo/interface/ITodo';
const App = () => {
const [todo, setTodo] = useState<ITodo[]>([]);
return (
<div className="max-w-4xl mx-auto mt-12">
<h2 className="text-3xl font-bold text-center">TODO</h2>
<TodoForm />
</div>
);
};
export default App;
Explanation
useState<ITodo>()
: Initializes a state variabletodo
with the typeITodo
.- This setup will allow the application to manage individual TODO items in the state.
Step 14: Define ITodoForm
Interface into ITodo.ts
In this step, you’ll define an interface for the TodoForm
component and update its functionality to manage the TODO list state.
import React from 'react';
export interface ITodoForm {
setTodos: React.Dispatch<React.SetStateAction<ITodo[]>>;
}
Updated ITodoForm
import React from 'react';
export interface ITodoForm {
setTodos: React.Dispatch<React.SetStateAction<ITodo[]>>;
}
setTodos
: A function to update the TODO list state.- Type:
React.Dispatch<React.SetStateAction<ITodo[]>>
- This type is provided by React and ensures the
setTodos
function is correctly typed.
- Type:
Update TodoForm
to Use ITodoForm
Modify the TodoForm
component to accept the setTodos
function as a prop.
Updated TodoForm.tsx
import React, { useState } from 'react';
import { ITodo, ITodoForm } from './interface/ITodo';
import { TextInput } from '@components/inputs';
import TodoSkinTextInput from './skins/TextInput.skin.todo';
import TodoSkinButton from './skins/Button.skin.todo';
const TodoForm = (props: ITodoForm) => {
const { setTodos } = props;
const [task, setTask] = useState<string>('');
const handleAddTodo = (e: React.FormEvent) => {
e.preventDefault();
if (task.trim()) {
setTodos((prev) => [...prev, { date: new Date(), task: task.trim() }]);
setTask('');
}
};
return (
<form className="mt-8" onSubmit={handleAddTodo}>
<label htmlFor="todo" className="text-sm/6 font-medium text-gray-900">
Add TODO
</label>
<TextInput
type="text"
name="todo"
id="todo"
placeholder="Enter your todo"
required
value={task}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTask(e.target.value)}
renderers={{ renderer: TodoSkinTextInput }}
/>
<div className="mt-3">
<button type="submit" className="w-full">
<TodoSkinButton value="Add Todo" />
</button>
</div>
</form>
);
};
export default TodoForm;
Explanation
- Added
ITodoForm
Prop:
- The
setTodos
function is passed as a prop to theTodoForm
component. - This allows
TodoForm
to update the parent component's TODO list state.
- Updated
handleAddTodo
Function:
- Checks if
task.trim()
is non-empty before updating the TODO list. - Creates a new TODO object with the current date and trimmed task.
- Resets the input field after adding the TODO.
- Typing Improvements:
- The
ITodoForm
interface ensures thesetTodos
prop is strongly typed, reducing potential errors.