Tungsten: Simple, TypeScript first, React-like Front End Framework
Tungsten is an experimental front end framework, similar to react. It began when I was dissatisfied with some aspects of react - especially the lack of a styling solution I was entirely happy with - I wanted to be able to create stylesheets for my components, in my component file (too much time searching for files otherwise), that was fully scoped. Svelte today has this, while it is not on offer for React. Tungsten today also has this.
I started out with some tweaks on top of React, that went well, but then decided to build it out from scratch to refine it further.
I have decided to match the workings of the framework to React, and support existing React syntax, which makes porting very quick (aside from a few features not yet supported).
I am starting to use the framework for projects, and seeing how it goes.
Key features:
- React-like front end framework
- Supports most React syntax
- Server side rendering out of the box
- A routing system based on a routes definition, which allows for data preloading per route
- Route based code splitting is planned
- Dev server using ES modules for speedy presentation (as used by vite) - no hot reloading yet
- Styling solution, allowing scoped stylesheets as part of each component. Supports CSS preprocessors
- New state interface for class based components, allowing assigning directly to a mutable state, cutting out nearly all of React's awkward state code (this.setState is still supported for back compatibility)
- Static site generation
- An option to bind inputs or components to a variable in state (or variables on the component outside of state)
- A built in global store that takes the place of redux, with a simple syntax
Create a simple counter component
import { StatefulComponent } from 'tungsten'type CounterProps = objecttype CounterState = {count: number}class Counter extends StatefulComponent<CounterProps, CounterState> {state: CounterState = {count: 0}content = () => (<div><h1>{this.state.count}</h1><button onClick={ () => this.state.count++ }>+</button></div>)style = Sass`divbackground: blackh1color: whitebuttoncolor: whitebackground: purple`}
- Specify Prop and State types
Props must be an object, so use 'object' if there are no props.type CounterProps = objecttype CounterState = {count: number} - Tungsten apps do not use constructor(). Declare fields as class properties.
If you need to do more at initialization, you can declare an init callbackstate: CounterState = {count: 0}init => () => { /* do stuff */ } - Update state with simple assignment, no need for setState() (setState will be added in the future for compatibility with legacy React components).
State is mutable, changes to state are immediately available, but component updates are batched for performance. (setState behaviour will match existing behaviour).onClick={ () => this.state.count++ } - Style the component with styles field. This example uses the SASS indented syntax language. SCSS (conventional SASS with curly braces) and plain CSS are also available.
Class and element based styles automatically applied. Use CSS-psuedo selectors too. style subcomponents by entering their component name like an html element name. The colorizer on this website does not colorize this code, however highlighting is available in VS Code.style = Sass`divbackground: blackh1color: whitebuttoncolor: whitebackground: purple`
0
Type-safe binding
Let's goWhich gives us:import { StatefulComponent, BindableInput } from 'tungsten'import { InputKeyboardEvent } from 'tungsten/types/jsx/html.events.js'type State = {name: stringage: number | undefined}export class Binding extends StatefulComponent<object, State> {state: State = {name: '',age: undefined}numberize = (newValue: string, event: InputKeyboardEvent) => { // filter a value to a number or undefinedlet n: number | undefined = parseFloat(newValue)if (isNaN(n)) n = undefinedreturn n // return the filtered value, which eventually will be set to this.state.age}content = () => (<><form><BindableInput placeholder="name" bind={[this.state, 'name']} /><br /><BindableInput placeholder="age" bind={[this.state, 'age']} filter={this.numberize} /></form><div>Name: {this.state.name}</div><div>Age: {this.state.age}</div></>)}
(note: this is using the vanilla OnChange event, so will require clicking out to update. work on events to come.)
'bind='' takes a type of [object, string]. In this example we bind to 'this.state.x', but we could bind to other variables such as this.x, or arbitrary objects. Of course, only binding to state will trigger component updates. filter= allows is to specify a function to process the value returned, and can be used to prevent or change input.
Easy type-safe global store
We can create a type-safe global store, by:1. creating a class that describes the shape of our store, with default value, called the schema.
2. Calling the 'registerStore' to register the schema, which returns a function to get the store for thecurrent app instance 3.
3. Defining a StoreComponent class that extends a base class, and loads the store on initialization.
This code will exist in the starter template.
The usable store is formed from the schema. Each entry 'entry = initialValue' is transformed into (psuedocode):import {type ComponentPropsBase, type ComponentStateConstraint,registerStore, BaseSimpleStoreComponent, BaseStatefulStoreComponent} from 'tungsten'// define a store schema, specifying entry types and default valuesclass StoreSchema {username: string = 'Bob'profilePicture: string = '/images/profile-picture.jpg'}// leave this as-isconst getStore = registerStore(StoreSchema)export abstract class SimpleStoreComponent<Props extends ComponentPropsBase = object,RouteData = undefined> extends BaseSimpleStoreComponent<Props, RouteData, StoreSchema> {store = getStore(this)}export abstract class StatefulStoreComponent<Props extends ComponentPropsBase = object,RouteData = undefined> extends BaseStatefulStoreComponent<Props, RouteData, StoreSchema> {store = getStore(this)}
Once we have the store schema and base class defined, we can easily create a new component with store accessentry: {value: initialValue,get = () => value,set = (newValue: Type) => value = newValue,subscribe = () => 'subscribes to updates',sync = () => {'subscribes to updates'return value}}
By usingimport { StoreComponent } from '../Store.js'export class Profile extends StoreComponent {content = () => <section><img src={this.store.profilePicture.sync()} /><br /><span>{this.store.username.sync()}</span></section>style = Sass`imgheight: 4em`}
we get the current values for those store entries, and also cause the Profile to update with the new values if they are changed.this.store.profilePicture.sync()this.store.username.sync()
<Profile /><br /><UpdateProfile /><br />

Bob
Function Components
Function components for simple components are supported. Support for a wider array of hooks is a (low priority) work in progress.
type Props = {name: string}export const Hello = ({name}: Props) => (<h3>Hello {name}!</h3>)Hello.style = Sass`h3color: blue`
<Hello name="Jim">
Hello Jim!
More
The client is currently 44.5KiB minified, uncompressed, however features are still being added.
Read the complete docs (WIP)