Skip to main content

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 Initial Page


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 Layout


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 the task state when the user types in the input field.
  • useState<string>: A React Hook that manages state in functional components. The <string> type ensures the task 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:

  1. The input field is always synchronized with the task state.
  2. 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.
note

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.

danger

You should pass the renderer. If it's not passed it will throw an error like No Renderer Specified

Output Error in Renderer

  • 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 of TextInputProps 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.
note

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 Success in Renderer


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;
danger

You should pass the renderer. If it's not passed it will throw an error like No Renderer Specified

Output Error in Renderer

  • 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 of ButtonProps 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.
note

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 Success in Renderer


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: A Date object representing when the TODO was created.
  • task: A string 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 variable todo with the type ITodo.
  • 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.

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

  1. Added ITodoForm Prop:
  • The setTodos function is passed as a prop to the TodoForm component.
  • This allows TodoForm to update the parent component's TODO list state.
  1. 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.
  1. Typing Improvements:
  • The ITodoForm interface ensures the setTodos prop is strongly typed, reducing potential errors.