๐Ÿฑ

Widget Sliced Design

An approach for conveniently and quickly building modular, scalable frontend applications of any size.
ย 
โš ๏ธ
The examples use React + TypeScript, BUT you can use absolutely anything: any language, platform, or point in the space-time continuum.
WSD naturally mirrors any system that can be described as: โ€œA tree-like UI that can be manipulated with functions and can work with variablesโ€ โ€” yes, in exactly that crude wording. That is why it has worked, works now, and will keep working.

From the author

notion image
ย 
Hello, dear reader. My name is David Shekunts, and I am a Tech Lead with 12 years of experience.
WSD is one of the best discoveries of my programming career. It maps so naturally onto how frontend is structured that it fits almost any task.
I have been using this approach for about 7 years, and I have never had a case where it created problems or failed me.
Does this sound like magic / a silver bullet? I may be a fool, but damn it, I will say โ€œyes.โ€ Surprisingly, I have not yet found a case where this structure does not work.
And now, dear reader, I challenge you to a duel: find such a case, show it to me, and do the most important thing in human history โ€” destroy the first and last silver bullet.
๐Ÿ“ฎ
Subscribe on X channel [ $davids.sh ] , where announcements of new chapters and updates to existing ones will be posted.

Example applications

Applications written using WSD:
ย 
(coming soon)
ย 

Glossary

ย 
  • Entity โ€” a description of some data. There are 3 types:
    • UI โ€” data of a specific UI component: whether it is visible, what is written in an input, etc. This is often 80% of all code.
    • Util โ€” geometry (Point in space, Triangle), an external API (Stripe), and everything else predefined by the external world.
    • Business โ€” the state of business entities your application works with. Very often, it comes from the backend and later goes back into the backend database.
ย 
// Entities are described through some kind of typing, whether type, interface, or class // Example of a UI Entity type LoginPopup = { closed: boolean collapsed: boolean loading: boolean error: string } // Example of a Util Entity: Triangle type Point = { x: number; y: number; } type Triangle = { one: Point; two: Point; three: Point; } // Example of a Util Entity: Stripe API type StripeCreateInvoceRequest = { id: string total: number } type StripeCreateInvoceResponse = { success: boolean message: string } // Example of a Business Entity type Product = { name: string price: number comments: Comment[] } type Cart = { products: Product[] totalSum: number }
ย 
  • State โ€” a particular combination of Entities in a particular Component at a specific moment in time.
ย 
// loginPopup is already State based on the LoginPopup UI entity const loginPopup = useState<LoginPopup>({ ... }) // State of util code for a triangle const pointOne = useState<Point>({ x: 10, y: 10 }) // ... const triangle = useState<Triangle>({ one: pointOne, ... }) // State of the Cart business entity const cart = useState<Cart>({ ... })
ย 
  • Logic โ€” a set of functions that do something with State. Usually there are 3 categories:
    • UI โ€” logic for the state of a specific UI component: whether it is visible, what is written in an input, etc. This is often 80% of all code.
    • Util โ€” for example, functions for converting centimeters to meters, calculating a hypotenuse through cosine, converting dollars to rubles, and everything else predefined by the external world: physics, laws, mathematics, etc.
    • Business โ€” functions that work with the Business Entities of your specific application (Put Product into Cart, Delete Comment). This often involves working with the backend.
ย 
// UI logic โ€” works with interface state const openLoginPopup = (popup: LoginPopup): LoginPopup => ({ ...popup, closed: false, }) const closeLoginPopup = (popup: LoginPopup): LoginPopup => ({ ...popup, closed: true, error: "", }) const setLoginError = ( popup: LoginPopup, error: string, ): LoginPopup => ({ ...popup, error, loading: false, }) // Util logic โ€” knows nothing about your application const getDistance = (a: Point, b: Point): number => { return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) } const getTrianglePerimeter = (triangle: Triangle): number => { return ( getDistance(triangle.one, triangle.two) + getDistance(triangle.two, triangle.three) + getDistance(triangle.three, triangle.one) ) } // Business logic โ€” works with the application's business entities const getCartTotalSum = (cart: Cart): number => { return cart.products.reduce((sum, product) => { return sum + product.price }, 0) } const addProductToCart = ( cart: Cart, product: Product, ): Cart => { const products = [...cart.products, product] return { ...cart, products, totalSum: getCartTotalSum({ ...cart, products }), } } const removeProductFromCart = ( cart: Cart, productName: string, ): Cart => { const products = cart.products.filter(product => { return product.name !== productName }) return { ...cart, products, totalSum: getCartTotalSum({ ...cart, products }), } }
ย 
  • Component โ€” a UI component with any variant or combination of logic:
type CartWidgetProps = { user: User | null cart: Cart openAuthPopup: () => void } const CartWidget = ({ user, cart, openAuthPopup, }: CartWidgetProps) => { const [loading, setLoading] = useState(false) const [error, setError] = useState("") const pay = async () => { if (!user) { openAuthPopup() return } setLoading(true) setError("") const response = await createStripeInvoice({ id: user.id, total: cart.totalSum, }) setLoading(false) if (!response.success) { setError(response.message) return } window.location.href = response.paymentUrl } return ( <div> <div>Products: {cart.products.length}</div> <div>Total: {cart.totalSum}</div> {error && <div>{error}</div>} <button disabled={loading} onClick={pay}> {loading ? "Loading..." : "Pay"} </button> </div> ) }
ย 

Splitting the application into 5 parts

ย 
  1. pages โ€” Components with State and Business Logic that are opened through some URL. You could also say: โ€œa widget for a URL.โ€
  1. widgets โ€” Components with State and Business Logic that are reused across different pages.
  1. apps โ€” allow us to assemble different applications from different sets of pages, for example for different URLs, different roles, or different environments such as web and Electron.js.
  1. libs โ€” code as libraries that could just as well live in npm, but you have reasons to keep them locally.
ย 

Structuring rules

This is where a very important concept appears: โ€œLevel.โ€
ย 
One of the huge frontend problems is how to structure code so that reusable code is conveniently available and understandable where it is reused, while not being present where it is not needed.
ย 
You could phrase it like this: โ€œI want to be able to delete 1 folder and completely destroy an entire feature with itโ€ โ€” or: โ€œI want to move 1 folder from one project to another and get fully working functionality.โ€
ย 
To implement this modularity, we need Levels โ€” application structuring rules that define what can interact with what.
ย 
src/ โ”œโ”€โ”€ apps/ # application assembly points โ”‚ โ””โ”€โ”€ shop/ โ”‚ โ””โ”€โ”€ index.tsx โ”‚ โ”œโ”€โ”€ pages/ # pages bound to URLs โ”‚ โ”œโ”€โ”€ home/ โ”‚ โ”‚ โ””โ”€โ”€ index.tsx โ”‚ โ””โ”€โ”€ cart/ โ”‚ โ””โ”€โ”€ index.tsx โ”‚ โ”œโ”€โ”€ widgets/ # globally reusable widgets โ”‚ โ”œโ”€โ”€ auth-widget/ โ”‚ โ”‚ โ””โ”€โ”€ index.tsx โ”‚ โ””โ”€โ”€ cart-widget/ โ”‚ โ””โ”€โ”€ index.tsx โ”‚ โ””โ”€โ”€ libs/ # local libraries โ”œโ”€โ”€ ui-kit/ โ”‚ โ””โ”€โ”€ index.ts โ”œโ”€โ”€ auth-sdk/ โ”‚ โ””โ”€โ”€ index.ts โ””โ”€โ”€ math-fns/ โ””โ”€โ”€ index.ts
Here are the rules that define how Levels interact:
ย 
# This may look complicated, but it becomes intuitive very quickly. # Literally look at how folders are nested inside each other โ€” look at the indentation, # and everything becomes clear. src/ โ”œโ”€โ”€ apps/ # not considered, because apps cannot be reused; this is the highest point of the entire hierarchy โ”‚ โ”œโ”€โ”€ libs-1/ # level 0 (root) โ”‚ โ”œโ”€โ”€ widgets/ โ”‚ โ””โ”€โ”€ widget-1/ # level 0 (root), parent of widget-2 โ”‚ โ”œโ”€โ”€ libs-2/ # level 1, child of widget-1 โ”‚ โ””โ”€โ”€ widget-2/ # level 1, child of widget-1 โ”‚ โ””โ”€โ”€ pages/ โ””โ”€โ”€ page-1/ # level 0 (root), parent of widget-3 and page-2 โ”œโ”€โ”€ libs-3/ # level 1, child of page-1, sibling of page-2, cousin of widget-2 and libs-2 โ”œโ”€โ”€ widget-3/ # level 1, child of page-1, sibling of page-2, cousin of widget-2 and libs-2 โ””โ”€โ”€ pages/ โ””โ”€โ”€ page-2/ # level 1, child of page-1, sibling of widget-3 and libs-3, cousin of widget-2 and libs-2, parent of widget-4 โ””โ”€โ”€ widget-4/ # level 2, child of page-2
ย 
  1. Level X is the folder of a specific widgets / pages / libs, and each nested folder increases X by 1.
    1. Child โ€” nested widgets / pages / libs.
    2. Parent โ€” the folder that contains the current one.
    3. Sibling โ€” folders located directly next to the current one.
    4. Cousin โ€” a folder located in another widgets / pages / libs branch at the same nesting level as the current one. The same rules apply to its descendants.
  1. Root Level (level 0) is where the highest-level apps, pages, widgets, and libs live.
  1. What can be placed where:
    1. libs can be placed inside any folder.
    2. widgets can be placed inside any folder, even inside other widgets.
    3. pages can be placed only inside pages.
    4. apps can exist only at the root level.
  1. Reuse rules:
    1. Nothing can reuse Cousins โ€” if something is in a separate branch, you cannot touch it.
    2. Everything may use parent, child, and sibling widgets.
    3. Everything may use parent and sibling libs.
    4. libs may not use anything except other libs from parent levels.
    5. pages may use only pages nested inside them, not parent pages and not sibling pages.

Additional notes

ย 
  1. In practice, a widget / page may consist of just an index.ts file that contains both Logic and UI.
  1. If you have only 1 app, you can simply create index.ts in the root of src and avoid creating an apps folder.
  1. If you want to create a widget that has no Component, then it should be created as a library in libs.
  1. Do not create folders like widget / page if you do not use them.
ย 

Private namespace

If you were publishing a library, for example with API types or an entire SDK for your system, how would it live in npm? Most likely as @${company}/some-sdk.
ย 
As I said above, libs is literally npm. Therefore, inside libs, you should create an @${company} folder and add libraries that are specifically about your application there.
ย 
src/ โ””โ”€โ”€ libs/ โ””โ”€โ”€ @company/ # private project libraries โ”œโ”€โ”€ ui-kit/ # design system โ”‚ โ””โ”€โ”€ index.ts โ”œโ”€โ”€ backend-sdk/ # backend API client โ”‚ โ””โ”€โ”€ index.ts โ””โ”€โ”€ analytics-sdk/ # analytics client โ””โ”€โ”€ index.ts
ย 

Without Levels

If Levels are unclear to you, or if you are not yet sure what should live where, then everything is very simple: put Widgets into the top-level widgets, Pages into the top-level pages, and Libraries directly into libs.
ย 
# Put everything right here: src/ โ”œโ”€โ”€ apps/ โ”‚ โ”œโ”€โ”€ app-1/ โ”‚ โ””โ”€โ”€ app-2/ โ”‚ โ”œโ”€โ”€ pages/ โ”‚ โ”œโ”€โ”€ page-1/ โ”‚ โ””โ”€โ”€ page-2/ โ”‚ โ”œโ”€โ”€ widgets/ โ”‚ โ”œโ”€โ”€ widget-1/ โ”‚ โ””โ”€โ”€ widget-2/ โ”‚ โ””โ”€โ”€ libs/ โ”œโ”€โ”€ lib-1/ โ””โ”€โ”€ lib-2/
ย 

Additional

tests

Tests can also live at any level. The important thing is that they live next to the thing they test. Again, we are trying to โ€œmove all related code by moving a folder,โ€ so tests must live next to the code they cover.
ย 
src/ โ”œโ”€โ”€ apps/ โ”‚ โ””โ”€โ”€ shop/ โ”‚ โ”œโ”€โ”€ index.tsx โ”‚ โ””โ”€โ”€ tests.ts # application assembly test โ”‚ โ”œโ”€โ”€ tests/ โ”‚ โ””โ”€โ”€ auth-flow.ts # global e2e test โ”‚ โ”œโ”€โ”€ libs/ โ”‚ โ””โ”€โ”€ @company/ โ”‚ โ””โ”€โ”€ backend-sdk/ โ”‚ โ”œโ”€โ”€ index.ts โ”‚ โ””โ”€โ”€ tests.ts # SDK test โ”‚ โ”œโ”€โ”€ widgets/ โ”‚ โ””โ”€โ”€ auth-widget/ โ”‚ โ”œโ”€โ”€ index.tsx โ”‚ โ””โ”€โ”€ tests.tsx # widget test โ”‚ โ””โ”€โ”€ pages/ โ””โ”€โ”€ home/ โ”œโ”€โ”€ index.tsx โ”œโ”€โ”€ tests.tsx # page test โ”œโ”€โ”€ libs/ โ”‚ โ””โ”€โ”€ home-api/ โ”‚ โ””โ”€โ”€ index.ts โ””โ”€โ”€ widgets/ โ””โ”€โ”€ cart-widget/ โ”œโ”€โ”€ index.tsx โ””โ”€โ”€ tests.tsx # local widget test

Final structure

A hypothetical online store:
ย 
src/ โ”œโ”€โ”€ apps/ โ”‚ โ”œโ”€โ”€ shop/ # shop application โ”‚ โ”‚ โ””โ”€โ”€ index.tsx โ”‚ โ””โ”€โ”€ admin-panel/ # admin panel application โ”‚ โ””โ”€โ”€ index.tsx โ”‚ โ”œโ”€โ”€ tests/ โ”‚ โ””โ”€โ”€ auth-flow.ts # global auth e2e test โ”‚ โ”œโ”€โ”€ libs/ โ”‚ โ”œโ”€โ”€ @company/ โ”‚ โ”‚ โ””โ”€โ”€ backend-sdk/ # backend API SDK โ”‚ โ”‚ โ”œโ”€โ”€ index.ts โ”‚ โ”‚ โ””โ”€โ”€ tests.ts โ”‚ โ””โ”€โ”€ stripe/ # local wrapper around Stripe โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ”œโ”€โ”€ widgets/ โ”‚ โ””โ”€โ”€ auth-widget/ # global authentication widget โ”‚ โ”œโ”€โ”€ index.tsx โ”‚ โ””โ”€โ”€ tests.tsx โ”‚ โ””โ”€โ”€ pages/ โ””โ”€โ”€ home/ # / page โ”œโ”€โ”€ index.tsx โ”œโ”€โ”€ tests.tsx โ”œโ”€โ”€ libs/ โ”‚ โ””โ”€โ”€ home-api/ โ”‚ โ””โ”€โ”€ index.ts โ”œโ”€โ”€ widgets/ โ”‚ โ””โ”€โ”€ cart-widget/ # local cart widget โ”‚ โ”œโ”€โ”€ index.tsx โ”‚ โ””โ”€โ”€ tests.tsx โ””โ”€โ”€ pages/ โ””โ”€โ”€ product/ # /product/:productId page โ”œโ”€โ”€ index.tsx โ””โ”€โ”€ widgets/ โ””โ”€โ”€ product-description/ โ””โ”€โ”€ index.tsx
ย 

Related concepts

ย 
  • FALSe โ€” an application structure that describes a similar approach, but for backend applications.
  • Feature-Sliced Design โ€” also tries to structure frontend by semantic layers and slices, but WSD is simpler: fewer terms, fewer levels, and less room for interpretation. I would not even recommend FSD, because everyone interprets it differently, and loose interpretations always prove that it is too complex and not truly โ€œunderstandable.โ€
  • Vertical Slice Architecture โ€” the idea of keeping all code for one feature nearby: UI, state, logic, and tests. It is similar to the WSD rule: โ€œmove the folder โ€” move the feature.โ€
  • Colocation โ€” the principle of keeping related code nearby: component, state, tests, styles, local utils.
  • Locality of Behavior โ€” logic should live near the place where it is actually used, not in some abstract global folder.
  • Package by Feature โ€” structure by user/business capabilities rather than by technical file types.
ย 
๐Ÿ“ฎ
Subscribe on X channel [ $davids.sh ] , where announcements of new chapters and updates to existing ones will be posted.