npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@nbottarini/react-presenter

v0.12.2

Published

Implement Humble Object Pattern to do sub-cutaneous unit testing without depending on e2e tests or fragile UI tests

Downloads

188

Readme

npm License: MIT CI Status

React presenters

Implement Humble Object Pattern to do sub-cutaneous unit testing without depending on e2e tests or fragile UI tests.

Installation

Npm:

$ npm install --save @nbottarini/react-presenter

Yarn:

$ yarn add @nbottarini/react-presenter

Introduction

Testing the UI logic is always difficult because of its fragility, indeterminism and slowness. It's a good practice to implement the Humble Object Pattern to separate the view logic from the view itself. The view logic can be encapsulated in a Presenter object that contains all the actions that can be made in the view, the state that it handles and the logic to operate and transform it. These Presenter objects are implemented in pure javascript/typescript code without depending on a specific view technology (like React or Vue). You can then test the presenter objects using regular unit tests.

In react the view is a React Component and a Presenter is a pure javascript/typescript class where you delegate all the component logic. The Component is only in charge of aesthetics and choosing the best UI and UX to implement the user desired actions. The presenter must be agnostic of which UI component or aesthetic was chosen. This allows the presenters to be independent from React. This way you can change the UI without breaking the logic, and you can also upgrade or change the UI technology by keeping the presenters intact.

The presenter exposes the state to the view by using a ViewModel. A ViewModel is simple model that has all the data that the view should display already formatted and processed. It usually consists of string properties.

The logic is delegated to a presenter class by using the usePresenter hook.

usePresenter(presenterFactory, startArgs)

This hook receives 2 parameters:

  • presenterFactory: a function that creates the presenter instance. It receives an onChange callback to notify React when the view model has changed. The presenter is constructed only once.
  • startArgs: arguments that are passed to the presenter start method.

A presenter can optionally implement a start method that is called when the view is mounted. It's also called when the startArgs changes (internally it uses a useEffect so expect the same behaviour). Another optional method is the stop method. This is called before the view is unmounted or before the startArgs are changed.

Finally, if using react navigation, the presenter can optionally implement a resume method when the view gains focus and a pause method when the view loses focus.

Basic Usage:

Counter example

Counter.tsx:

import React from 'react'
import { usePresenter } from '@nbottarini/react-presenter'

const useCounterPresenter = () => usePresenter((onChange) => new CounterPresenter(onChange))

export const Counter: React.FC = () => {
    const presenter = useCounterPresenter()
    return (
        <div>
            <span id="counter">Counter: {presenter.model.value}</span>
            <button onClick={() => presenter.increment()}>Increment</button>
        </div>
    )
}

CounterPresenter.ts:

import { PresenterBase, ChangeFunc } from '@nbottarini/react-presenter'

export class CounterPresenter extends PresenterBase<CounterVM> {
    constructor(onChange: ChangeFunc) {
        super(onChange)
        this._model = { value: 0 }
    }

    increment() {
        this.updateModel({ value: this.model.value + 1 })
    }
}

export interface CounterVM {
    value: number
}

CounterPresenter.test.ts:

it('starts with 0', () => {
    const presenter = new CounterPresenter(onChange)
    
    expect(presenter.model.value).toEqual(0)
})

it('increment increments value by 1', () => {
    const presenter = new CounterPresenter(onChange)
    presenter.increment()
    
    presenter.increment()

    expect(presenter.model.value).toEqual(2)
})

const onChange = () => {}

User Detail example

UserDetail.tsx:

const useUserDetailPresenter = (startArgs) => {
    const apiClient = useApiClient()
    return usePresenter((onChange) => new UserDetailPresenter(onChange, apiClient), startArgs)
}

export const UserDetail: React.FC = (props) => {
    const userId = props.route.params['userId']
    const presenter = useUserDetailPresenter([userId])
    if (presenter.model.isLoading) return <Loader />
    const user = presenter.model.user!!
    return (
        <DetailContainer>
            <DetailRow title="Name" value={user.name} />
            <DetailRow title="Balance" value={user.balance} />
            <DetailRow title="Created At" value={user.createdAt} />
        </DetailContainer>
    )
}

UserDetailPresenter.ts:

export class UserDetailPresenter extends PresenterBase<UserDetailVM> {
    constructor(onChange: ChangeFunc, private apiClient: ApiClient) {
        super(onChange)
        this._model = { isLoading: true, user: null }
    }
    
    async start(userId: number) {
        this.updateModel({ isLoading: true })
        const user = await this.apiClient.getUser(userId)
        const userVM = this.toUserVM(user)
        this.updateModel({ isLoading: false, user: userVM })
    }
    
    private toUserVM(user: User): UserVM {
        return {
            name: user.name,
            balance: MoneyFormatter.format(user.balance),
            createdAt: DateFormatter.format(user.createdAt),
        }
    }
}

export interface UserDetailVM {
    isLoading: boolean
    user: UserVM|null
}

export interface UserVM {
    name: string
    balance: string
    createdAt: string
}