Web Dev Fundamentals: REST, JSON, MongoDB, React, Redux
REST API and HTTP Methods
REST API (Representational State Transfer API)
A REST API is a set of web services that follow the principles of REST, which is an architectural style for distributed systems. REST APIs are commonly used for building web services that allow communication between clients and servers over HTTP.
Key principles of REST:
Stateless: Every request from a client to a server must contain all the information needed to understand and process the request. The server does not store any session information.
Client-Server: The client and server are separate entities that communicate over a network. This separation allows for scalability and flexibility.
Uniform Interface: A standardized way of communicating between client and server (using URLs, HTTP methods, etc.).
Resource-Based: Resources (such as data) are identified by URLs, and each resource is accessed and manipulated using standard HTTP methods.
HTTP Methods in REST APIs
In REST APIs, the following HTTP methods are used to perform CRUD operations (Create, Read, Update, Delete):
GET:
- Used to retrieve data from the server (Read).
- It is a safe and idempotent method, meaning it does not alter the server’s state and can be repeated without side effects.
- Example:
GET /users
– Retrieves a list of users.
POST:
- Used to send data to the server to create a new resource (Create).
- Example:
POST /users
– Creates a new user with the data provided in the request body.
PUT:
- Used to update an existing resource on the server (Update).
- Example:
PUT /users/1
– Updates the user with ID 1.
DELETE:
- Used to delete a resource from the server (Delete).
- Example:
DELETE /users/1
– Deletes the user with ID 1.
PATCH:
- Used to partially update an existing resource (Partial Update).
- Example:
PATCH /users/1
– Partially updates the user with ID 1.
Example
GET /products
– Retrieves all products.POST /products
– Creates a new product.PUT /products/123
– Updates the product with ID 123.DELETE /products/123
– Deletes the product with ID 123.
JSON and JSON Parsing Explained
JSON (JavaScript Object Notation)
JSON is a lightweight data-interchange format that is easy for humans to read and write, and easy for machines to parse and generate. It is often used for transmitting data between a server and a client in web applications, especially in REST APIs.
A JSON object is a collection of key-value pairs enclosed in curly braces {}
. The values can be strings, numbers, arrays, booleans, or other objects.
Example of JSON
{
"name": "John",
"age": 30,
"isStudent": false,
"courses": ["Math", "Science", "History"]
}
- Key-Value Pairs:
"name": "John"
is a key-value pair where the key is “name” and the value is “John”. - Array:
"courses": ["Math", "Science", "History"]
is an array that contains multiple values.
JSON Parsing
JSON parsing is the process of converting a JSON-formatted string into a JavaScript object or converting a JavaScript object into a JSON-formatted string. This is commonly done in JavaScript for working with data received from APIs.
JSON.parse()
: Converts a JSON string into a JavaScript object.JSON.stringify()
: Converts a JavaScript object into a JSON string.
JSON Parsing Example in JavaScript
// JSON string
const jsonString = '{"name": "John", "age": 30, "isStudent": false}';
// Parse the JSON string into a JavaScript object
const jsonObject = JSON.parse(jsonString);
console.log(jsonObject.name); // Output: John
// Convert a JavaScript object to a JSON string
const newJsonString = JSON.stringify(jsonObject);
console.log(newJsonString); // Output: {"name":"John","age":30,"isStudent":false}
JSON.parse()
converts the JSON stringjsonString
into a JavaScript object.JSON.stringify()
converts the JavaScript objectjsonObject
back into a JSON string.
MongoDB: Database, Document, Collection
MongoDB
MongoDB is a NoSQL database that stores data in a flexible, JSON-like format called BSON (Binary JSON). It is schema-less, meaning you don’t have to define a fixed structure for your data before inserting it.
Components of MongoDB
Database:
- A database is a container for collections. Each database has its own set of collections and documents. You can have multiple databases in MongoDB, each for different purposes.
- Example:
mydb
,userDB
, etc.
Collection:
- A collection is a grouping of MongoDB documents. It is analogous to a table in a relational database.
- A collection does not enforce a schema, meaning documents within the same collection can have different fields.
- Example:
users
,products
,orders
are collections.
Document:
- A document is a single record in a collection. It is represented as a BSON (Binary JSON) object.
- Documents contain key-value pairs, and each document can have its own structure.
- Example:
{ "_id": ObjectId("605c72ef1532075f1c6e90e2"), "name": "John", "age": 30, "email": "john@example.com" }
Mongo Shell Commands
To Create a New Database
In MongoDB, you don’t explicitly create a database. A database is created when you insert data into a collection. However, you can switch to a database using the use
command.
use myDatabase // Switches to the 'myDatabase' database. If it doesn't exist, it will be created.
Create a New Collection
To create a new collection, you can use the db.createCollection()
method.
db.createCollection('myCollection') // Creates a new collection called 'myCollection'
Find a Document
To find documents in a collection, you can use the find()
method.
db.myCollection.find() // Finds all documents in 'myCollection'
db.myCollection.find({ name: "John" }) // Finds documents where the 'name' field is "John"
Delete a Document
You can use the deleteOne()
or deleteMany()
method to delete documents from a collection.
db.myCollection.deleteOne({ name: "John" }) // Deletes the first document where 'name' is "John"
db.myCollection.deleteMany({ age: 30 }) // Deletes all documents where 'age' is 30
React Components: Functional & Class
React Component
A React component is a reusable, self-contained piece of UI that can have its own state, properties (props), and lifecycle methods (for class components). Components are the building blocks of a React application and are used to describe the appearance and behavior of the UI.
There are two main types of components in React:
- Functional Components
- Class Components
Functional Components
Functional components are simple JavaScript functions that return JSX (HTML-like syntax). They do not have lifecycle methods or state before React 16.8, but with the introduction of Hooks, functional components can now have state and side effects.
Example of a Functional Component
import React, { useState } from 'react';
function Counter() {
// Using the useState Hook to manage state in a functional component
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
useState
Hook: In this example, we are using theuseState
Hook to manage the state of the component (the count).- Functional components are easier to write and typically used for simpler UI elements, especially with Hooks.
Class Components
Class components are more complex. They are ES6 classes that extend React.Component
and can have methods like render()
, lifecycle methods (componentDidMount
, componentWillUnmount
, etc.), and a local state.
Example of a Class Component
import React, { Component } from 'react';
class Counter extends Component {
constructor(props) {
super(props);
// Initializing state in class components
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
export default Counter;
- State: In class components, the state is initialized in the constructor using
this.state
. render()
Method: This method is required in class components and returns JSX.this.setState()
: Used to update the state in class components.
Differences:
- Class components are more powerful because they can handle lifecycle methods and manage state.
- Functional components, however, are simpler and now can also manage state with the help of Hooks.
React Form Handling: Controlled & Uncontrolled
Form Handling in React
Form handling in React is done through two main types of components: Controlled and Uncontrolled components. The key difference between them is how the form data is managed (in the component’s state or directly in the DOM).
Controlled Components
In a controlled component, the form data is controlled by the React component’s state. Every time the input changes, the state is updated, and the component re-renders with the new state.
Example of Controlled Component
import React, { useState } from 'react';
function ControlledForm() {
const [name, setName] = useState('');
const handleChange = (e) => {
setName(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
alert('Submitted Name: ' + name);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" value={name} onChange={handleChange} />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default ControlledForm;
In this example:
- The input field’s value is controlled by the
name
state. - Every time the input value changes, the state is updated via the
handleChange
function. - The form submission is handled using
handleSubmit
, which accesses the state data.
Advantages of Controlled Components:
- The React state serves as the “single source of truth” for the form inputs.
- More predictable and easier to debug.
Uncontrolled Components
In an uncontrolled component, the form data is handled by the DOM itself, and React does not directly manage it. You can use ref
to access form values directly without storing them in state.
Example of Uncontrolled Component
import React, { useRef } from 'react';
function UncontrolledForm() {
const nameRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
alert('Submitted Name: ' + nameRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" ref={nameRef} />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default UncontrolledForm;
In this example:
- The
ref
is used to access the value of the input field directly from the DOM. - The
nameRef.current.value
gives us the input value when the form is submitted.
Advantages of Uncontrolled Components:
- They are simpler when you don’t need to track or manipulate the form data in the state.
- Useful for simpler forms or when interacting with non-React libraries that manage the form input.
React State Explained
React State
In React, state is an object that represents the parts of the app that can change over time. The state is local to the component and can be used to store dynamic data (like user input, responses from an API, etc.). When the state changes, React re-renders the component to reflect the updated UI.
Key Points about State
- State is mutable: Unlike props, which are read-only, state can be changed (or mutated) by the component itself.
- State is local: Each component can have its own state.
- State triggers re-renders: When the state changes, React automatically re-renders the component to reflect the updated data.
Example of React State
import React, { useState } from 'react';
function Counter() {
// useState is a Hook that allows us to manage state in a functional component
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // Updating the state
};
return (
<div>
<p>Current Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default Counter;
In this example:
useState(0)
initializes the statecount
with an initial value of0
.setCount(count + 1)
updates the state value, which triggers a re-render of the component with the new count.- The state variable
count
is used to display the current count in the UI.
When to Use State
- When you want to store dynamic values that change over time (like input fields, user interactions, or server responses).
- State is used for interactive components, like forms, counters, modals, etc.
Summary:
- Props are used to pass data from a parent to a child component (immutable).
- State is used to store and manage data within a component (mutable).
Redux State Immutability
State in Redux is Immutable
In Redux, the state is immutable. This means that when you update the state, you do not directly modify the state object; instead, you return a new state object based on the current state. This immutability ensures that state changes are predictable and traceable, which helps with debugging and prevents accidental side-effects.
Immutability in Redux is essential because:
- It allows for predictable state transitions.
- It makes it easier to implement features like undo/redo and time travel debugging.
- It ensures that components can re-render only when necessary (i.e., when the state has actually changed).
How Does Immutability Work in Redux?
In Redux, the reducer is responsible for updating the state. When an action is dispatched, the reducer takes the current state and the dispatched action as arguments and returns a new state object. The key principle is that the reducer should never mutate the state directly but instead create and return a new object with the updated values.
Example of Immutable State in Redux
// Initial state
const initialState = {
count: 0
};
// Reducer function
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
// Instead of modifying the state directly, return a new object
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
}
// Example of Redux store configuration
import { createStore } from 'redux';
const store = createStore(counterReducer);
// Dispatch actions
store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // { count: 1 }
store.dispatch({ type: 'DECREMENT' });
console.log(store.getState()); // { count: 0 }
- In the example, when the
INCREMENT
action is dispatched, instead of modifyingstate.count
directly, we use the spread operator (...state
) to copy the existing state and then create a new state object with an updatedcount
value. - The new state object is returned, and the Redux store updates the state.
Why Immutability is Important
- Predictability: You can always track the changes in state because the previous state is never altered directly.
- Performance Optimization: Since the state is immutable, you can easily determine whether the state has changed by comparing references, which helps with performance optimizations.
- Debugging: Tools like Redux DevTools rely on immutability to track state changes and allow features like time travel debugging.
Redux Thunk Explained
Redux Thunk
Redux Thunk is a middleware for Redux that allows you to write action creators that return functions (instead of plain action objects). These functions can be used for delayed actions, conditional dispatching, or even making asynchronous requests (e.g., fetching data from an API).
By default, Redux only supports synchronous actions, meaning actions should return plain objects. However, using Redux Thunk, you can dispatch functions that are asynchronous and allow for side effects (like making an API call) before dispatching regular actions.
How Redux Thunk Works
- Redux Thunk allows you to write action creators that can return either:
- A plain action object (like a regular action).
- A function (which is called with
dispatch
andgetState
as arguments). The function can perform asynchronous operations like API requests and then dispatch normal actions based on the result.
Example of Using Redux Thunk
Let’s say we want to fetch data from an API and update the state based on the response.
1. Installing Redux Thunk
First, you need to install redux-thunk
if you don’t have it already:
npm install redux-thunk
2. Redux Store Setup with Thunk
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
// Initial state
const initialState = {
data: [],
loading: false,
error: null,
};
// Reducer
function dataReducer(state = initialState, action) {
switch (action.type) {
case 'FETCH_DATA_REQUEST':
return { ...state, loading: true };
case 'FETCH_DATA_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_DATA_FAILURE':
return { ...state, loading: false, error: action.error };
default:
return state;
}
}
// Create store with thunk middleware
const store = createStore(dataReducer, applyMiddleware(thunk));
export default store;
3. Async Action Creator with Thunk
Now, let’s create an asynchronous action that fetches data from an API.
// Async action creator using redux-thunk
const fetchData = () => {
return (dispatch) => {
// Dispatching an action to indicate the request has started
dispatch({ type: 'FETCH_DATA_REQUEST' });
// Making an asynchronous API call
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => {
// Dispatching success action after data is received
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data });
})
.catch((error) => {
// Dispatching failure action if there's an error
dispatch({ type: 'FETCH_DATA_FAILURE', error: error.message });
});
};
};
- In this example, the
fetchData
action creator returns a function (because of Thunk). - The function first dispatches a
FETCH_DATA_REQUEST
action to indicate that the data is being loaded. - Then it makes an asynchronous API call using
fetch
. When the request is complete, it dispatches either aFETCH_DATA_SUCCESS
action with the data or aFETCH_DATA_FAILURE
action in case of an error.
4. Dispatching the Async Action
To use the async action in your app, you can dispatch it like a regular action:
store.dispatch(fetchData());
Benefits of Redux Thunk
- Asynchronous Actions: It makes it possible to handle asynchronous operations (like API calls) directly within your action creators.
- Delayed Actions: You can perform side effects before dispatching an action, allowing you to control the flow of data.
- Conditional Dispatching: You can conditionally dispatch actions based on certain logic or the current state.
Summary of Redux Thunk
- Redux Thunk allows you to write action creators that return functions instead of plain action objects.
- These functions can perform asynchronous operations (like fetching data) before dispatching actions.
- It’s extremely useful for handling side effects in Redux (such as making API requests).
Conclusion
- Redux state is immutable: You should never modify the state directly but instead return a new state object from reducers.
- Redux Thunk: A middleware for handling asynchronous actions in Redux by allowing action creators to return functions (instead of plain objects), which helps in dispatching actions after performing asynchronous operations.
Redux Data Flow Explained
Redux Application Data Flow
Redux follows a unidirectional data flow pattern where the state of an application is predictable and consistent. This flow ensures that every action results in a new state, and the components react to that state change.
The flow of data in a Redux application happens in the following sequence:
View (UI):
- The UI (React Components) displays data to the user.
- The UI can trigger actions based on user interactions (like clicking buttons, submitting forms, etc.).
Action:
- An action is a plain JavaScript object that describes a change that should occur in the state. It has at least a
type
property, and optionally apayload
containing the data necessary for the state update. - The action is dispatched from the UI to inform the store about what kind of state change is requested.
- An action is a plain JavaScript object that describes a change that should occur in the state. It has at least a
Action Creator (optional but recommended):
- An action creator is a function that creates and returns the action object. It abstracts the creation of the action to ensure consistency in action structures.
Reducer:
- The reducer is a pure function that takes the current state and the dispatched action as arguments and returns a new state.
- Reducers should be pure functions, meaning they don’t mutate the state, they return a new state based on the previous state and action.
Store:
- The Redux store holds the application’s state and allows the UI to access the state or dispatch actions.
- The store invokes reducers to update the state based on dispatched actions.
UI (React Components):
- The UI components subscribe to the store and re-render when the state changes.
Diagram of Redux Data Flow
+---------------------+ +-----------------+ +--------------------+
| User Interacts | ---> | Dispatching | ---> | Reducer |
| with UI | | Actions | | (Updating State) |
+---------------------+ +-----------------+ +--------------------+
| | |
| v v
| +--------------------------------------------+
| | Redux Store |
| | (Holds the state and allows access) |
| +--------------------------------------------+
| |
| v
+---------------------> +---------------------------+
| React Components |
| (Re-render with updated |
| state) |
+---------------------------+
Pure Functions Explained
What is a Pure Function?
A pure function is a function that:
- Always returns the same output for the same input (no randomness or external dependencies).
- Has no side effects, meaning it does not modify anything outside the function (e.g., it doesn’t change global variables, mutate input arguments, or perform I/O operations like writing to a file).
Pure functions are predictable, easy to test, and maintain. They enable referential transparency, which means a function call can be replaced with its return value without changing the program’s behavior.
Characteristics of Pure Functions
- Deterministic: Given the same input, the function will always produce the same output.
- No Side Effects: The function does not alter any external state, global variables, or interact with I/O (like changing a database, logging, etc.).
Example of a Pure Function
// Pure function example
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // Output: 5
console.log(add(2, 3)); // Output: 5 (always the same output for the same inputs)
In this example:
- The function
add
always returns the same result for the same inputsa
andb
(i.e., adding two numbers). - It does not change any external state, nor does it depend on any external variables.
- This makes
add
a pure function.
Example of an Impure Function
let externalState = 0;
// Impure function example
function increment() {
externalState += 1;
return externalState;
}
console.log(increment()); // Output: 1
console.log(increment()); // Output: 2 (depends on external state, result is not the same for the same input)
In this example:
- The function
increment
modifies the external variableexternalState
, which makes it impure. - The function doesn’t return the same result for the same inputs because it depends on and modifies external state.
Why Use Pure Functions?
- Predictability: Pure functions always return the same output for the same inputs, making the application more predictable.
- Testability: Since pure functions don’t depend on external state, they are easier to unit test.
- Concurrency: Pure functions are easier to parallelize since they don’t rely on shared state.
- Debugging: They are easier to debug because there are no side effects that affect other parts of the system.
Summary
- Redux Data Flow follows a predictable, unidirectional flow where actions are dispatched, reducers update the state, and the UI reacts to the state change.
- Pure Functions are deterministic and have no side effects, which makes them easy to reason about and test. In contrast, impure functions can modify external state or have non-deterministic behavior.