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

react-entity-normalize-station

v1.4.0

Published

normalize & denormalize entity with normalizr, jotai use react hook!

Downloads

20

Readme

React entity normalize station

Simple and easy, type safe normalize & denormalize entity building system with normalizr, jotai, zustand and react hook!

| ✅ In this document; we call EntityStation is about build system(configureNormalizeEntityStation) result!

Install

npm i react-entity-normalize-station --save

Example

Interface

export interface User {
  id: number;
  name: string;
}

export interface Comment {
  id: number;
  body: string;
  author: User;
  preview_replys?: Comment[];
}

export interface Post {
  id: number;
  body: string;
  author: User;
  likeCount: number;
  hasMyLike: boolean;
  preview_comments?: Comment[];
}

Configure EntityStation

// src/entity-station.ts
import { configureNormalizeEntityStation, createEntityModel } from 'react-entity-normalize-station';

const users = createEntityModel<User>('users')();
const comments = createEntityModel<Comment>('comments')(self => ({
  preview_replys: [self], // example of recursion using
  author: users
}));
const posts = createEntityModel<Post>('posts')({
  author: users,
  preview_comments: [comments]
});

export const {
  useDenormalize,
  useEntitys,
  useNormalizeEntity,
  useNormalizeHandler
} = configureNormalizeEntityStation({
  users,
  comments,
  posts
});

Usage (fetch data with react-query)

import {
  useDenormalize,
  useEntitys,
  useNormalizeEntity,
  useNormalizeHandler
} from './entity-station';
import { useQuery, useMutation } from 'react-query';

declare const fetchPosts: () => Promise<Post[]>;
declare const toggleLikePost: (postId: number) => Promise<void>;

function Post ({ postId }: { postId: number; }) {
  const [post, setPost] = useDenormalize('posts', postId);
  function toggleLike () {
    setPost({
      ...post,
      likeCount: post.likeCount + (post.hasMyLike ? -1 : 1),
      hasMyLike: !post.hasMyLike
    });
  }
  const likeMutation = useMutation(() => toggleLikePost(post.id), {
    onMutate: toggleLike,
    onError: toggleLike
  });
  return (
    <div>
      {post.body}
      <input
        type="checkbox"
        checked={post.hasMyLike}
        onChange={() => likeMutation.mutate()}
      />
      (Like : {post.likeCount})
    </div>
  )
}

export default Example () {
  const postQuery = useQuery('postQuery', fetchPosts, {
    suspense: true
  });
  const [posts] = useNormalizeEntity('posts', postQuery.data);
  return (
    <div>
      {posts.map(post => <Post post={post} key={post.id} />)}
    </div>
  )
}

MobX (mobx-react-lite) integration

You can use this package with mobx-react-lite, useLocalObservable to connect entity station.

Step 1, import react-entity-normalize-station/mobx to configure EntityStation! It will be add useEntityObservable at the default entity station

import { configureNormalizeEntityStation } from 'react-entity-normalize-station/mobx';

// ... Model setting (users, comments, posts) ...

export const { useEntityObservable } = configureNormalizeEntityStation({
  users,
  comments,
  posts
});

Step 2, You can use useEntityObservable similar like useLocalObservable. But be explicit what property is connect to normalize entity. Typescript will help property setting.

interface PostStoreInterface {
  currentPosts: Posts[];
  fetchPosts(): Promise<void>;
}

const StoreProvider = () => {
  // const postStore = useLocalObservable(PostStore);
  const postStore = useEntityObservable(PostStore, {
    currentPosts: 'posts'
  });
  // ... set context provider
};

Now, the EntityStation should intercept original property to change computed value. you can preservation your componentcode base to change it!

⚠️ Warning to use MobX integration

When using this integration, entity station will change observable value to computed value. That mean you can't use MobX magicall syntax. Let's find out migrate mobx store.

Using array case

When you want update the array, the reference will be change.

// Before
runInAction(() => {
  this.posts.push(newPost);
});

// After
runInAction(() => {
  this.posts = [...this.posts, newPost];
});

Using object case

When you want update the object, reference will be change or just use normalize or produceEntity from EntityStation.

// Before
runInAction(() => {
  this.post.isLike = true;
});

// After
import { normalize, produceEntity } from 'src/entity-station.ts';

runInAction(() => {
  this.post = {
    ...this.post,
    isLike: true
  }
  // or
  normalize('posts', {
    ...this.post,
    isLike: true
  });
  // or
  produceEntity('posts', posts => {
    const post = posts[this.post.id];
    if (post) {
      post.isLike = true;
    }
  });
});

API

createEntityModel<T>(name): (definition, options) => Entity

This function is type safe version of normalizr(new entity.Schema). It will be use partial function to set model type at first generic, definition and options type inference at second function.

// src/entity.ts
import { createEntityModel } from 'react-entity-normalize-station';

const users = createEntityModel<User>('users')({}, {
  idAttribute: 'userId'
});
const comments = createEntityModel<Comment>('comments')(self => ({
  author: users
  reply_preview: [self]
}));

Type EntityRecord

This type is interface of entityStore data. If you want to use this type at your project, can use like,

// src/entity.ts
import type { EntityRecord } from 'react-entity-normalize-station';

const models = { users, comments }; // result of createEntityModel

export type AppEntityRecord = EntityRecord<typeof models>;

Type EntityModel

This type is record of entity model. If you want to use this type at your project, can use like,

// src/entity.ts
import type { EntityModel } from 'react-entity-normalize-station';

const models = { users, comments }; // result of createEntityModel

export type AppEntityModel = EntityModel<typeof models>;

// ...
type UserTypeFromAppEntityModel = AppEntityModel['users']; // User
type CommentTypeFromAppEntityModel = AppEntityModel['comments']; // User

configureNormalizeEntityStation(models) => EntityStation

This function build EntityStation. It's create entity store, normalize & denormalize functions, entity producer, react custom hooks, initail provider.

// src/entity-station.ts
import { configureNormalizeEntityStation } from 'react-entity-normalize-station';

export const {
  entityStore,
  normalize,
  denormalize,
  produceEntity,
  useNormalizeEntity,
  useDenormalize,
  useEntitys,
  NormalizeEntityProvider
} = configureNormalizeEntityStation({
  users,
  comments
});

EntityStation.entityStore

This store create by zustand/vanilla. If you want to get state of entity record, subscribe entity change without react, use this store.

import { entityStore } from 'src/entity-station.ts';

const unsubscriber = entityStore.subsribe(entities => {
  console.debug('Entity update', { entities });
  // or console.debug('Entity update', { entities: entityStore.getState() });
});

EntityStation.normalize(modelKey: EntityKey, data: Model | Model[]) => IdType | IdType[]

import { normalize } from 'src/entity-station.ts';

declare const MOCK_COMMENT: Comment; // { id: number }
declare const MOCK_USER: User; // { userId: string }

// When model id is number
const commentId = normalize('comments', MOCK_COMMENT); // return `number`
const commentIds = normalize('comments', [MOCK_COMMENT]); // return `number[]`

// When model id is string
const userId = normalize('users', MOCK_USER); // return `string`
const userIds = normalize('users', [MOCK_USER]); // return `string[]`
  • This function work update normalize entity and return id.
  • Normalize entity will update entityStore.
  • This function is type safe of infer Model & id type

EntityStation.denormalize(modelKey: EntityKey, data: IdType | IdType[]) => (Model | undefined) | Model[]

import { denormalize } from 'src/entity-station.ts';

// When model id is number
const comment = denormalize('comments', 1); // return `Comment | undefined`
const comments = denormalize('comments', [1]); // return `Comment[]`

// When model id is string
const user = denormalize('users', 'asdf'); // return `User | undefined`
const users = denormalize('users', ['asdf']); // return `User[]`
  • This function work denormalize entity store and parameter data
  • This function is type safe of infer Model & id type

EnityStation.produceEntity(modelKey: EntityKey, callback: (records: Record<IdType, NormalizeModel>) => unknown) => void

This function is produce entity record by immer.

import { produceEntity } from 'src/entity-station.ts';

produceEntity('comments', comments => {
  const comment = comments[1];
  if (comment) {
    comment.isLike = true;
    comment.likeCount ++;
  }
});

EntityStation.useNormalizeEntity(modelKey: EntityKey, data: Model | Model[]) => [(Model | undefined) | Model[]), setter]

This function similar by EntityStation.normalize, but will return denormalize result. You can use at react function component and set the data

import { useNormalizeEntity } from 'src/entity-station.ts';

function CommentList () {
  const commentResult = useSuspenseFetch('https://.../comments'); // This hook is psuedo of fetching data suspense & refresh
  const [comments, setComments] = useNormalizeEntity('comments', commentResult.data);
  const handleRefreshComment = useCallback(async () => {
    await commentResult.refresh();
    setComments(commentResult.data);
  }, []);
  return (
    <div>
      {comments.map(comment => (
        <Comment comment={comment} key={comment.id} />
      ))}
      <button onClick={handleRefreshComment}>Refresh</button>
    </div>
  );
}

EntityStation.useDenormalize(modelKey: EntityKey, data: IdType | IdType[]) => [(Model | undefined) | Model[]), setter]

This function similar by EntityStation.denormalize. You can use at react function component and set the data

import { useDenormalize } from 'src/entity.ts';

function CommentLikeButton ({ commentId }: { commentId: number }) {
  const [comment, setComment] = useDenormalize('comments', commentId);
  const handleToggleLike = useCallback(() => {
    setComment({
      ...comment,
      isLike: !comment.isLike,
      likeCount: comment.likeCount + (comment.isLike ? -1 : 1)
    });
  }, [comment, setComment]);
  return (
    <CommentLikeIcon
      isLike={comment.isLike}
      onClick={handleToggleLike}
    />
  );
}

EnityStation.useEntitys(): EntityRecords

If you want to use entity records at react hook, use this!

import { useEntitys } from 'src/entity.ts';

function useEntityUpdateDebug () {
  const entities = useEntitys();
  useEffect(() => {
    console.debug('Entity update!', { entities });
  }, [entities]);
}

EntityStation.NormalizeEntityProvider({ initialEntityRecord: EntityRecord }): React.ComponentType

If you want to initial entity store(like using ssr), using this provider. This provider is not required when you don't need initial entity at your app.

import { NormalizeEntityProvider } from 'src/entity.ts';

function App(props) {
  return (
    <NormalizeEntityProvider initialEntityRecord={INITIAL_ENITY_RECORD}>
      {props.children}
    </NormalizeEntityProvider>
  );
}

EntityStation.subscribe(modelKey: EntityKey, data: IdType | IdType[]): Unsubscriber(() => void)

If you want to subscribe change model data, you can use this subscriber. Return type is subscriber function. This function use at mobx integration.

import { subscribe } from 'src/entity.ts';

subscribe('comments', [1, 2, 3], () => {
  console.log('Comment [1, 2, 3] is changed!');
});