Module-04 FSD (BIS601) Search Creators
Module-04 FSD (BIS601) Search Creators
Module-04
React State
10.Next Steps
Initial State
o The state of a component is captured in a variable called this.state in
the component’s class, which should be an object consisting of one or
more key-value pairs, where each key is a state variable name and the
value is the current value of that variable.
o React does not specify what needs to go into the state, but it is useful
to store in the state anything that affects the rendered view and can
change due to any event. These are typically events generated due to
user interaction.
For now, let’s just use an array of issues as the one and only state of the component
and use that array to construct the table of issues.
Thus, in the render() method of IssueTable, let’s change the loop that creates the
set of IssueRows to use the state variable called issues rather than the global array
like this
We can have other state variables, for instance if we were showing the issue list in
multiple pages, and we wanted to also keep the page number currently being
shown as another state variable, we could have done that by adding another key to
the object like page: 0. The set of all changes to use the state to render the view of
IssueTable is shown in Listing 4-1
constructor() {
super();
this.state = { issues: [] };
}
loadData() {
setTimeout(() => {
this.setState({ issues: initialIssues });
}, 500);
}
componentDidMount() {
this.loadData();
}
8. Key Takeaways
If you refresh the browser (assuming you’re still running npm run watch and npm
start on two different consoles), you will find that the list of issues is displayed as it
used to be in the previous steps. But, you will also see that for a fraction of a
second after the page is loaded, the table is empty, as shown in Figure 4-1
Updating State
1. Changing a Portion of the State Instead of Overwriting It
• The method assigns an ID and creation date before adding the issue:
createIssue(issue) {
issue.id = this.state.issues.length + 1;
issue.created = new Date();
}
3. Directly Modifying State is NOT Allowed
this.state.issues.push(issue); // Incorrect
issues = this.state.issues;
issues.push(issue);
this.setState({ issues: issues }); // Incorrect
4. Why Can't We Modify State Directly?
• This ensures React recognizes a new reference and updates the component
properly.
const sampleIssue = {
status: 'New',
owner: 'Pieta',
title: 'Completion date should be optional',
};
setTimeout(() => {
this.createIssue(sampleIssue);
}, 2000);
This should automatically add the sample issue to the list of issues after the page is
loaded. The final set of changes—for using a timer to append a sample issue to the
list of issues—is shown in Listing 4-3
On running this set of changes and refreshing the browser, you’ll see that there are
two rows of issues to start with. After two seconds, a third row is added with a
newly generated ID and the contents of the sample issue. A screenshot of the three-
row table is shown in Figure 4-2
Note that we did not explicitly call a setState() on the IssueRow components. React
automatically propagates any changes to child components that depend on the
parent component’s state. Further, we did not have to write any code for inserting a
row into the DOM. React calculated the changes to the virtual DOM and inserted a
new row. At this point, the hierarchy of the components and the data flow can be
visually depicted, as shown in Figure 4-3.
Lifting State Up
In React, Lifting State Up is a pattern used to manage shared state between
multiple components. Instead of keeping state separately in sibling components,
we move it to their closest common parent. This allows child components to
receive the shared state as props and update it via callback functions provided by
the parent.
setTimeout(() => {
this.props.createIssue(sampleIssue);
}, 2000);
This setup allows IssueAdd to trigger a new issue addition without directly
modifying the state. Instead, it calls createIssue() in IssueList, ensuring state
updates in a controlled way.
Event Handling
Let’s now add an issue interactively, on the click of a button rather than use a timer
to do this. We’ll create a form with two text inputs and use the values that the user
enters in them to add a new issue. An Add button will trigger the addition. Let’s
start by creating the form with two text inputs in the render() method of IssueAdd
in place of the placeholder div
At this point, clicking Add will submit the form and fetch the same screen again.
That’s not what we want. Firstly, we want it to call createIssue() using the values
in the owner and title fields. Secondly, we want to prevent the form from being
submitted because we will handle the event ourselves.
So, let’s rewrite the form declaration with a name and an on Submit handler like
this.
Since handleSubmit will be called from an event, the context, or this will be set to
the object generating the event, which is typically the window object. Since
handleSubmit will be called from an event, the context, or this will be set to the
object generating the event, which is typically the window object.
The new full code of the IssueAdd class, after these changes, is shown in Listing
4-7
You can now test the changes by entering some values in the owner and title fields
and clicking Add. You can add as
many rows as you like. If you add two issues, you’ll get a screen like the one in
Figure 4-5.
At the end of all this, we have been able to encapsulate and initiate the creation of
a new issue from the IssueAdd component itself. This new UI hierarchy data and
function flow is depicted in Figure 4-6.
Stateless Components
Stateless components, also called functional components, are components that
only receive props and render UI without maintaining any internal state.
• Improved Performance: They are faster because they don’t have lifecycle
methods or state updates.
• Cleaner Code: They are simpler and easier to read.
• Better Maintainability: They focus only on rendering, making debugging
easier.
Designing Components
Designing Components Most beginners will have a bit of confusion between state and props, when to
use which, what granularity of components should one choose, and how to go about it all. This section is
devoted to discussing some principles and best practices.
Anything that can change due to an event anywhere in the component hierarchy
qualifies as being part of the state. Avoid keeping computed values in the state;
instead, simply compute them when needed, typically inside the render() method.
Component Hierarchy
• Split the application into components and subcomponents.
• Typically, this will reflect the data model itself. For example, in the Issue
Tracker, the issues array was represented by the IssueTable component, and
each issue was represented by the IssueRow component.
• Decide on the granularity just as you would for splitting functions and
objects. The component should be self-contained with minimal and logical
interfaces to the parent.
• If you find it doing too many things, just like in functions, it should
probably be split into multiple components, so that it follows the Single
Responsibility principle (that is, every component should be responsible for
one and only one thing).
• If you are passing in too many props to a component, it is an indication that
either the component needs to be split, or it need not exist: the parent itself
could do the job
Communication
• Communication between components depends on the direction. Parents
communicate to children via props; when state changes, the props
automatically change.
• Children communicate to parents via callbacks. Siblings and cousins can’t
communicate with each other, so if there is a need, the information has to go
up the hierarchy and then back down.
• This is called lifting the state up. This is what we did when we dealt with
adding a new issue. The IssueAdd component had to insert a row in
IssueTable.
• It was achieved by keeping the state in the least common ancestor, IssueList.
The addition was initiated by IssueAdd and a new array element added in
IssueList’s state via a callback.
• The result was seen in IssueTable by passing the issues array down as props
from IssueList.
Stateless Components
• In a well-designed application, most components would be stateless
functions of their properties. All states would be captured in a few
components at the top of the hierarchy, from where the props of all the
descendants are derived.
• We did just that with the IssueList, where we kept the state. We converted
all descendent components to stateless components, relying only on props
passed down the hierarchy to render themselves.
• We kept the state in IssueList because that was the least common
component above all the descendants that depended on that state.
• Sometimes, you may find that there is no logical common ancestor. In such
cases, you may have to invent a new component just to hold the state, even
though visually the component has nothing
Express
1. What is Express?
Express.js is a minimal and flexible web framework for Node.js. It provides
essential web functionalities through middleware and enables efficient routing.
2. Routing in Express
• The HTTP method (GET) and path (/hello) define the route.
• The handler function processes the request and sends a response.
Route Parameters:
Example:
app.get('/api/issues', handler); // Specific
app.use('/api/*', middleware); // Generic (placed after)
6. Middleware in Express
Application-Level Middleware:
app.use((req, res, next) => {
console.log('Request received');
next(); // Pass control to next middleware
});
Built-in Middleware:
app.use(express.static('public'));
Path-Specific Middleware:
app.use('/public', express.static('public'));
REST API
1. What is REST?
REST (Representational State Transfer) is an architectural pattern for
designing web APIs. It focuses on:
Resources (nouns, such as customers or orders)
HTTP methods (verbs like GET, POST, PUT, DELETE)
Stateless communication (each request contains all required information)
2. REST is Resource-Based
Although the HTTP method and operation mapping are well mapped and specified,
REST by itself lays down no rules for the following:
GraphQL
While REST follows a structured API design with multiple endpoints, GraphQL
introduces a single endpoint that enables clients to request only the data they
need.
{
user(id: "1234") {
name
email
}
}
{
"data": {
"user": {
"name": "Alice",
"email": "[email protected]"
}
}
}
{
user(id: "1") {
name
issues {
title
status
}
}
}
type User {
id: ID!
name: String!
email: String!
}
Libraries
• Parsing and dealing with the type system language (also called the GraphQL
Schema Language) as well as the query language is hard to do on your own.
• Fortunately, there are tools and libraries available in most languages for this
purpose.
• For JavaScript on the back-end, there is a reference implementation of
GraphQL called GraphQL.js. To tie this to Express and enable HTTP
requests to be the transport mechanism for the API calls, there is a package
called express-graphql.
• But these are very basic tools that lack some advanced support such as
modularized schemas and seamless handling of custom scalar types.
• The package graphql-tools and the related apollo-server are built on top of
GraphQL.js to add these advanced features. We will be using the advanced
packages for the Issue Tracker application in this chapter.
• I will cover only those features of GraphQL that are needed for the purpose
of the application.
• For advanced features that you may need in your own specific application,
do refer to the complete documentation of GraphQL at https://graphql.org
and the tools at https://www.apollographql.com/ docs/graphql-tools/.
Example Schema:
const typeDefs = `
type Query {
about: String! // Read operation
}
type Mutation {
setAboutMessage(message: String!): String // Write operation
}
`;
• about: A mandatory (!) string field that provides the about message.
• setAboutMessage(message: String!): A mutation that updates the about
message.
3. Creating Resolvers
Resolvers are functions that define how each GraphQL field should behave.
Example Resolvers:
let aboutMessage = "Issue Tracker API v1.0"; // Initial message
const resolvers = {
Query: {
about: () => aboutMessage, // Returns the message
},
Mutation: {
setAboutMessage(_, { message }) {
• Query Resolver:
o about: Returns the aboutMessage string.
• Mutation Resolver:
o setAboutMessage: Updates aboutMessage with the new value.
This creates a GraphQL server instance using the schema (typeDefs) and resolvers.
app.listen(3000, function () {
console.log('App started on port 3000');
});
http://localhost:3000/graphql
Response:
"data": {
"about": "Issue Tracker API v1.0"
}
}
2. Update the about message:
mutation {
setAboutMessage(message: "New API Version")
}
Response:
{
"data": {
"setAboutMessage": "New API Version"
}
}
3. Verify the update:
query {
about
}
Response:
{
"data": {
"about": "New API Version"
}
}
Key Takeaways:
Potential Enhancements:
const fs = require('fs').promises;
const typeDefs = await fs.readFile('./server/schema.graphql', 'utf-8');
Now that you have learned the basics of GraphQL, let’s make some progress
toward building the Issue Tracker application using this knowledge. The next thing
we’ll do is implement an API to fetch a list of issues. We’ll test it using the
Playground and, in the next section, we’ll change the front-end to integrate with
this new API.
Let’s start by modifying the schema to define a custom type called Issue. It should
contain all the fields of the issue object that we have been using up to now. But
since there is no scalar type to denote a date in GraphQL, let’s use a string type for
the time being. We’ll implement custom scalar types later in this chapter. So, the
type will have integers and strings, some of which are optional. Here’s the partial
schema code for the new type.
Now, let’s add a new field under Query to return a list of issues. The GraphQL
way to specify a list of another type is to enclose it within square brackets. We
could use [Issue] as the type for the field, which we will call issueList.
But we need to say not only that the return value is mandatory, but also that each
element in the list cannot be null. So, we have to add the exclamation mark after
Issue as well as after the array type, as in [Issue!]!.
Let’s also separate the top-level Query and Mutation definitions from the custom
types using a comment. The way to add comments in the schema is using the #
character at the beginning of a line. All these changes are listed in Listing 5-5
To test this in the Playground, you will need to run a query that specifies the
issueList field, with subfields. But first, a refresh of the browser is needed so that
the Playground has the latest schema and doesn’t show errors when you type the
query.
Since we are using fetch(), we include a polyfill for Internet Explorer and older
browsers in index.html:
<script src="https://unpkg.com/@babel/polyfill@7/dist/polyfill.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/fetch.umd.js"></script>
• Convert the response JSON into a JavaScript object and update state:
• Initially, created and due were Date objects, but now they are strings.
• Fix: Remove .toDateString() and use the string values directly:
<td>{issue.created}</td>
<td>{issue.due}</td>
• Refresh the browser: The issue list will now be loaded from the API.
• The UI remains the same, except:
o Long date strings appear instead of formatted dates.
o Add operation does not work (we will fix this in the next section).
Storing dates as strings in the database or API responses is not ideal because:
1. Sorting and filtering become complex (since string-based sorting does not
work correctly for dates).
2. Time zone and localization issues (dates should be displayed in the user's
local time).
3. Standardization issues (different formats can cause inconsistencies).
GraphQL does not support Date natively, so we create a custom scalar type.
Modify schema.graphql
• Use scalar instead of type to define a new GraphQL type for dates.
• Update the Issue type to use GraphQLDate instead of String.
scalar GraphQLDate
type Issue {
id: Int!
title: String!
status: String!
owner: String
effort: Int
created: GraphQLDate!
due: GraphQLDate
}
Modify server.js
const resolvers = {
Query: {
about: () => aboutMessage,
issueList,
},
Mutation: {
setAboutMessage,
},
GraphQLDate, // Add custom scalar type
};
3. How This Works
API Response: When fetching issues, GraphQLDate will convert Date objects
to ISO 8601 strings.
API Input Handling: If a new issue is added with a date in ISO format, it will
be converted back to a Date object automatically.
4. Next Steps
Finally, we need to set this resolver at the same level as Query and Mutation (at the
top level) as the value for the scalar type GraphQLDate. The complete set of
changes in server.js is shown in Listing 5-10.
Now, we will add an API endpoint to create new issues in our in-memory
database. This involves:
1. Modify schema.graphql
First, define an input type IssueInputs (separate from the Issue type because it
does not include id or created).
type Mutation {
setAboutMessage(message: String!): String
issueAdd(issue: IssueInputs!): Issue!
}
Modify server.js
issue.id = issuesDB.length + 1;
issuesDB.push(issue);
return issue;
}
const resolvers = {
Query: {
about: () => aboutMessage,
issueList,
},
Mutation: {
setAboutMessage,
issueAdd, // New Mutation
},
GraphQLDate, // Custom Date scalar
};
},
parseValue(value) {
return new Date(value); // Convert ISO string to Date object
},
parseLiteral(ast) {
return ast.kind === Kind.STRING ? new Date(ast.value) : undefined;
}
});
Now, test the API by sending the following mutation request in GraphiQL or
Postman:
mutation {
issueAdd(issue: {
title: "GraphQL Issue",
owner: "Alice",
effort: 5,
due: "2025-02-15T12:00:00.000Z"
}) {
id
title
status
owner
effort
created
due
}
}
Expected Response
{
"data": {
"issueAdd": {
"id": 1,
"title": "GraphQL Issue",
"status": "New",
"owner": "Alice",
"effort": 5,
"created": "2025-02-12T10:30:00.000Z",
"due": "2025-02-15T12:00:00.000Z"
}
}
}
• We remove the default "status": "New" from the frontend since the backend
handles it.
• We set the due date to 10 days from today using JavaScript.
const issue = {
owner: form.owner.value,
title: form.title.value,
due: new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 10), // 10 days later
};
• Instead of adding the new issue to state.issues, we reload the entire issue
list.
• This ensures data accuracy in case of errors or concurrent updates.
5. Result
• The new issue is added with a due date of 10 days from now.
• Refreshing the page shows the new issue because it is stored on the server.
Query Variables
Instead of embedding dynamic values directly inside the query string, GraphQL
allows us to use query variables, which are passed separately in a JSON object.
This approach improves security, readability, and reliability by avoiding issues
with escaping special characters.
We name the mutation (setNewMessage) and replace static values with variables
(starting with $).
mutation {
setAboutMessage(message: "New About Message")
}
When executing this query (e.g., in GraphQL Playground or an API call), the
variables must be passed separately as JSON.
{
"message": "Hello World!"
}
const variables = {
issue: {
title: issue.title,
owner: issue.owner,
due: issue.due.toISOString()
}
};
Input Validations
1. Input Validations :Input validation ensures that user input meets specific criteria
before being processed by the server. In GraphQL, we can implement validation in
two primary ways:
a. Schema-Level Validations
enum StatusType {
New
Assigned
Fixed
Closed
}
type Issue {
status: StatusType!
}
input IssueInputs {
status: StatusType = New
}
b. Programmatic Validations
Example:
if (issue.title.length < 3) {
errors.push('Field "title" must be at least 3 characters long.');
}
if (errors.length > 0) {
throw new UserInputError('Invalid input(s)', { errors });
}
}
• Validating Dates:
o Ensuring the provided date is in the correct format.
o Example:
parseValue(value) {
const dateValue = new Date(value);
return isNaN(dateValue) ? undefined : dateValue;
}
Displaying Errors
Errors must be displayed properly in the UI to improve the user experience. We
handle two types of errors:
• These errors occur when there is a problem with the API request.
• Handled using a try-catch block around the fetch function.
• Example:
• These errors occur when the user provides invalid data (e.g., empty title,
missing owner).
• Displaying errors for user input:
if (result.errors) {
const error = result.errors[0];
1. Missing Title
{
"message": "Invalid input(s)",
"errors": ["Field \"title\" must be at least 3 characters long."]
}
2. Invalid Date
{
"message": "Expected type GraphQLDate, found \"not-a-date\"."
}
3. Invalid Status
{
"message": "Expected type StatusType, found \"Unknown\"."
}
To test transport errors, you can stop the server after refreshing the browser and
then try to add a new issue. If you do that, you will find the error message like the
screenshot in Figure 5-5.