Forgo is a lightweight (4KB) JavaScript library for creating web apps. Although it’s billed as an alternative to React, and even uses JSX syntax, there are several major differences to keep in mind while working with Forgo.
In this article, we’ll look at how to create client-side web apps with the Forgo library, and along the way, we’ll look at the differences between Forgo and React.
To get started, we should install webpack and webpack-cli globally by running:
npm i -g webpack webpack-cli
Then, we clone the JavaScript repo into our computer with the following command:
npx degit forgojs/forgo-template-javascript#main my-project
Once we’ve done that, we should jump into our my-project
folder and run npm i
to install the packages listed in package.json
. npm start
will run the project we just cloned.
By default, the project runs on port 8080. We can change the port on which we run the project by adding the -- --port=8888
option, like so:
npm start -- --port=8888
The project we cloned comes with a JSX parser and other build tools, so we don’t have to worry about the tooling for our project.
Now we can create a simple component by writing the following in index.js
:
import { mount, rerender } from "forgo"; function Timer() { let seconds = 0; return { render(props, args) { setTimeout(() => { seconds++; rerender(args.element); }, 1000); return <div>{seconds}</div>; }, }; }
We call rerender
with args.element
to re-render a component. Unlike React, the states are stored in regular variables rather than states, and we render the seconds
in the return
statement.
To mount the component, we’d add:
window.addEventListener("load", () => { mount(<Timer />, document.getElementById("root")); });
So, together with the sample component we just wrote to index.js
, we have:
import { mount, rerender } from "forgo"; function Timer() { let seconds = 0; return { render(props, args) { setTimeout(() => { seconds++; rerender(args.element); }, 1000); return <div>{seconds}</div>; }, }; } window.addEventListener("load", () => { mount(<Timer />, document.getElementById("root")); });
We can replace document.getElementById
with the element selector, so we can write:
window.addEventListener("load", () => { mount(<Timer />, "#root"); });
As in React, we can create child components and pass them props. To do that, we write:
import { mount } from "forgo"; function Parent() { return { render() { return ( <div> <Greeter name="james" /> <Greeter name="mary" /> </div> ); }, }; } function Greeter({ name }) { return { render(props, args) { return <div>hi {name}</div>; }, }; } window.addEventListener("load", () => { mount(<Parent />, document.getElementById("root")); });
The Greeter
component is a child of the Parent
component. Greeter
accepts the name
prop, which we add in the return
statement to render its value.
We can easily add inputs with Forgo as well. To do that, we’d write:
import { mount } from "forgo"; function Input() { const inputRef = {}; return { render() { function onClick() { const inputElement = inputRef.value; alert(inputElement.value); } return ( <div> <input type="text" ref={inputRef} /> <button onclick={onClick}>get value</button> </div> ); }, }; } window.addEventListener("load", () => { mount(<Input />, document.getElementById("root")); });
We create an object variable called inputRef
and pass it to the ref
prop of the input element. Then, we get the input element object with the inputRef.value
property, as we did in the onClick
method. Now we can get the value
property to get the input value.
Similar to React, we can render lists by calling map
on an array and then returning the components we want to render. For instance, we can write:
import { mount } from "forgo"; function App() { return { render() { const people = [ { name: "james", id: 1 }, { name: "mary", id: 2 }, ]; return ( <div> {people.map(({ key, name }) => ( <Child key={key} name={name} /> ))} </div> ); }, }; } function Child() { return { render({ name }) { return <div>hi {name}</div>; }, }; } window.addEventListener("load", () => { mount(<App />, document.getElementById("root")); });
We have an App
component with an array people
. We can render the array in the browser by calling map
and then returning the Child
component with the key
and name
props.
The key
prop allows Forgo to distinguish between the items rendered by assigning them a unique ID. name
is a regular prop we get in the Child
component and render.
We can fetch data with promises, just like we do with React components. However, we can only use the then
method to get data — no async/await. If we use async
and await
, we get a regeneratorRuntime not defined
error.
Thus, to get data when a component mounts, we can write something like this:
import { mount, rerender } from "forgo"; function App() { let data; return { render(_, args) { if (!data) { fetch('https://yesno.wtf/api') .then(res => res.json()) .then(d => { data = d; rerender(args.element); }) return <p>loading</p> } return <div>{JSON.stringify(data)}</div> }, }; } window.addEventListener("load", () => { mount(<App />, document.getElementById("root")); });
data
initially has no value, in which case we call fetch
to make a GET request for some data. Then, we assign the d
parameter to data
, which has the response data.
We call rerender
with args.element
to re-render the component after data
is updated, and in the next render cycle, the stringified data
value is rendered. Now we should be able to see the data we retrieved from the API.
A Forgo component emits various events throughout its render lifecycle. Let’s take the following component as an example:
import { mount } from "forgo"; function App() { return { render(props, args) { return <div id='hello'>Hello</div>; }, mount(props, args) { console.log(`mounted on node with id ${args.element.node.id}`); }, unmount(props, args) { console.log("unmounted"); }, }; } window.addEventListener("load", () => { mount(<App />, document.getElementById("root")); });
Our render
method returns a div with ID hello
, and when a component mounts, we call the mount
method. args.element.node.id
takes the value of the root element’s id
attribute, so its value should be 'hello'
.
Therefore, we should see 'mounted on node with id hello'
logged. The unmount
method runs when the component unmounts, so we should see 'unmounted'
when the component is removed from the DOM.
For instance, let’s say we have:
import { mount, rerender } from "forgo"; function App() { let showChild = false return { render(props, args) { const onClick = () => { showChild = !showChild rerender(args.element) } return ( <div> <button onclick={onClick}>toggle</button> {showChild ? <Child /> : undefined} </div> ); }, }; } function Child() { return { render(props, args) { return <div>child</div>; }, unmount(props, args) { console.log("unmounted"); }, }; } window.addEventListener("load", () => { mount(<App />, document.getElementById("root")); });
When we click the toggle button, the Child
component will disappear, and we should see 'unmounted'
.
We can also choose when to re-render a component by calling rerender
manually with the shouldUpdate
function. shouldUpdate
has parameters newProps
and oldProps
, which, as their names imply, give us the current and old values of the props, respectively.
Consider the following example:
import { mount, rerender } from "forgo"; function App() { let name = 'james' return { render(props, args) { const onClick = () => { name = name === 'james' ? 'jane' : 'james' rerender(args.element) } return ( <div> <button onclick={onClick}>toggle</button> <Greeter name={name} /> </div> ); }, }; } function Greeter() { return { render({ name }, args) { return <div>hi {name}</div>; }, shouldUpdate(newProps, oldProps) { console.log(newProps, oldProps) return newProps.name !== oldProps.name; }, }; } window.addEventListener("load", () => { mount(<App />, document.getElementById("root")); });
The toggle button toggles the value of name
, which we pass in as the value of Greeter
‘s name
prop. So, whenever we click the toggle button, we’ll see the console log update.
Finally, we can handle errors in the object we return with the error
method. Errors from child components bubble up to the parent.
So, if we have:
import { mount } from "forgo"; function App() { return { render() { return ( <div> <BadComponent /> </div> ); }, error(props, args) { return ( <p> {args.error.message} </p> ); }, }; } function BadComponent() { return { render() { throw new Error("error"); }, }; } window.addEventListener("load", () => { mount(<App />, document.getElementById("root")); });
args.error.message
should have the string that we passed into the Error
constructor since the error propagates from the child component to the parent.
At just 4KB, Forgo is a tiny alternative to React that allows us to create simple components with ease. Since it uses JSX syntax, it should feel familiar to React developers.
There are some major differences in behavior, however. Re-rendering has to be done manually with the rerender
method. Component lifecycles are also quite different, and states are stored as plain variables instead of states. There are also no hooks; instead, we use lifecycle methods to perform various actions during a component’s lifecycle.
The easiest way to get started with Forgo yourself is to install the webpack dev dependencies globally and then clone the starter project with degit
.
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowDesign React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.
From basic syntax and advanced techniques to practical applications and error handling, here’s how to use node-cron.