Skip to content

Commit

Permalink
Environment and doc updates (#36)
Browse files Browse the repository at this point in the history
* Update changelog formatter and some scaffolding

* Update scripts to match template

* Refine docs
  • Loading branch information
spautz committed Aug 27, 2022
1 parent 42dfbc2 commit 1a1ffe4
Show file tree
Hide file tree
Showing 15 changed files with 672 additions and 323 deletions.
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json",
"changelog": [
"changesets-format-with-git-links",
"changesets-format-with-issue-links",
{
"repoBaseUrl": "https://github.com/spautz/dynamic-selectors"
}
Expand Down
5 changes: 5 additions & 0 deletions .changeset/strong-nails-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dynamic-selectors/core": patch
---

Clean up docs
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dynamic Selectors

Selectors with parameters and dynamic dependencies.
Selectors with parameters and dynamic dependencies. See [Selector Comparison](https://github.com/spautz/dynamic-selectors/blob/main/packages/core/docs/comparison-with-reselect.md).

[![build status](https://github.com/spautz/dynamic-selectors/workflows/CI/badge.svg)](https://github.com/spautz/dynamic-selectors/actions)
[![test coverage](https://img.shields.io/coveralls/github/spautz/dynamic-selectors/main.svg)](https://coveralls.io/github/spautz/dynamic-selectors?branch=main)
Expand Down
37 changes: 18 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "@dynamic-selectors/workspace",
"private": true,
"version": "0.4.0",
"description": "Root workspace for Dynamic-Selectors",
"license": "MIT",
"homepage": "https://github.com/spautz/dynamic-selectors#readme",
Expand All @@ -18,69 +17,69 @@
"node": "^14 || ^16 || ^18",
"pnpm": "^7"
},
"packageManager": "pnpm@7.8.0",
"packageManager": "pnpm@7.9.5",
"type": "module",
"scripts": {
"____ HOOKS _________________________________________________________": "",
"preinstall": "npx only-allow pnpm",
"prepare": "husky install",
"husky:precommit": "lint-staged",
"husky:prepush": "pnpm run format:verify && pnpm run types && pnpm run lint",
"____ MAIN __________________________________________________________": "",
"test": "pnpm run test:coverage",
"____ BATCH COMMANDS FOR PACKAGES ___________________________________": "",
"packages:clean": "pnpm -r run clean",
"packages:clean": "pnpm --parallel -r run clean",
"packages:build": "pnpm -r run build",
"packages:test": "pnpm -r run test:coverage",
"packages:test:coverage": "pnpm -r run test:coverage",
"packages:test:quick": "pnpm -r run test:quick",
"packages:test:quick": "pnpm --parallel -r run test:quick",
"packages:all": "pnpm -r run all",
"packages:all:readonly": "pnpm -r run all:readonly",
"packages:all:quick": "pnpm -r run all:quick",
"packages:all:quick": "pnpm --parallel -r run all:quick",
"packages:all:ci": "pnpm -r run all:ci",
"____ INTEGRATION ___________________________________________________": "",
"clean": "pnpm run test:clean && rimraf ./node_modules/.cache ./*.log && pnpm run packages:clean",
"all": "pnpm run format && pnpm run types && pnpm run lint:fix && pnpm run test:coverage && pnpm run packages:build",
"all:readonly": "pnpm run format:verify && pnpm run types && pnpm run lint && pnpm run test:quick",
"all:quick": "pnpm run format && pnpm run types && pnpm run lint:fix",
"all:ci": "pnpm run format:verify && pnpm run types && pnpm run lint && pnpm run test:ci && pnpm run packages:build && pnpm run changelog:status",
"all:ci": "pnpm run format:verify && pnpm run types && pnpm run lint && pnpm run test:ci && pnpm run packages:build && pnpm run changelog:status:ci",
"____ INDIVIDUAL COMMANDS ___________________________________________": "",
"changelog": "changeset",
"changelog:status": "changeset status --since=origin/main --verbose",
"changelog:status": "changeset status --verbose",
"changelog:status:ci": "changeset status --since=origin/main --verbose",
"format": "prettier --write .",
"format:verify": "prettier --list-different .",
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint . --max-warnings 0 --fix",
"release:prep": "pnpm run changelog:status && changeset version",
"test": "pnpm run test:coverage",
"test:clean": "rimraf ./coverage",
"test:ci": "pnpm run test:clean && vitest run --coverage",
"test:coverage": "pnpm run test:clean && vitest run --coverage",
"test:quick": "pnpm run test:clean && vitest run --coverage=false",
"test:watch": "pnpm run test:clean && vitest watch --coverage=false",
"test:watchcoverage": "pnpm run test:clean && vitest watch --coverage",
"types": "tsc --p tsconfig.json --noEmit"
"types": "tsc -p ./tsconfig.json --noEmit"
},
"devDependencies": {
"@changesets/cli": "2.24.2",
"@changesets/cli": "2.24.3",
"@changesets/types": "5.1.0",
"@dynamic-selectors/core": "workspace:*",
"@dynamic-selectors/with-reselect": "workspace:*",
"@tsconfig/recommended": "1.0.1",
"@typescript-eslint/eslint-plugin": "5.32.0",
"@typescript-eslint/parser": "5.32.0",
"c8": "7.12.0",
"changesets-format-with-git-links": "0.2.0",
"@typescript-eslint/eslint-plugin": "5.35.1",
"@typescript-eslint/parser": "5.35.1",
"@vitest/coverage-c8": "0.22.1",
"changesets-format-with-issue-links": "0.3.0",
"downlevel-dts": "0.10.0",
"eslint": "8.21.0",
"eslint": "8.23.0",
"eslint-config-prettier": "8.5.0",
"gitlog": "4.0.4",
"husky": "8.0.1",
"lint-staged": "13.0.3",
"prettier": "2.7.1",
"rimraf": "3.0.2",
"tsup": "6.2.1",
"typescript": "4.7.4",
"vitest": "0.21.0"
"tsup": "6.2.3",
"typescript": "4.8.2",
"vitest": "0.22.1"
},
"lint-staged": {
"*.{css,html,js,jsx,json,less,md,scss,ts,tsx,yaml}": [
Expand Down
19 changes: 10 additions & 9 deletions packages/core/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# @dynamic-selectors/core

Selectors with parameters and dynamic dependencies. See [Selector Comparison](https://github.com/spautz/dynamic-selectors/blob/main/packages/core/docs/comparison-with-reselect.md).

[![npm version](https://img.shields.io/npm/v/@dynamic-selectors/core.svg)](https://www.npmjs.com/package/@dynamic-selectors/core)
[![build status](https://github.com/spautz/dynamic-selectors/workflows/CI/badge.svg)](https://github.com/spautz/dynamic-selectors/actions)
[![test coverage](https://coveralls.io/repos/github/spautz/dynamic-selectors/badge.svg?branch=x-cov-core)](https://coveralls.io/github/spautz/dynamic-selectors?branch=x-cov-core)
[![dependencies status](https://img.shields.io/librariesio/release/npm/@dynamic-selectors/core.svg)](https://libraries.io/github/spautz/dynamic-selectors)
[![gzip size](https://img.badgesize.io/https://unpkg.com/@dynamic-selectors/core@latest/dist/core.cjs.production.min.js?compression=gzip)](https://bundlephobia.com/result?p=@dynamic-selectors/core@latest)

Selectors with parameters and dynamic dependencies.
[![gzip size](https://img.badgesize.io/https://unpkg.com/@dynamic-selectors/core@latest/dist/index.js?compression=gzip)](https://bundlephobia.com/result?p=@dynamic-selectors/core)

Dynamic selectors can access state and call each other dynamically, even conditionally or within loops, without needing
to register dependencies up-front. As with Reselect and Re-reselect, functions are only re-run when necessary.
Expand All @@ -19,12 +20,12 @@ For more information or related packages, see the [Dynamic Selectors workspace](
```javascript
import { createDynamicSelector } from '@dynamic-selectors/core';

/* Simple selectors can access state, like normal */
// Simple selectors can access state, like normal
const getAuthor = createDynamicSelector((getState, authorId) => {
return getState(`authors[${authorId}]`);
});

/* Selectors can call other selectors inline -- even in loops */
// Selectors can call other selectors inline -- even in loops
const getBooksForAuthor = createDynamicSelector((getState, authorId) => {
const author = getAuthor(authorId);
if (author) {
Expand All @@ -33,14 +34,15 @@ const getBooksForAuthor = createDynamicSelector((getState, authorId) => {
// Else: throw, return default value, etc
});

/* Because dependencies are dynamic, selectors are easier to compose together */
// Because selector-to-selector calls are dynamic, it's easier to compose and reuse them
const getBooksForMultipleAuthors = createDynamicSelector((getState, authorIds) => {
return authorIds.map(getBooksForAuthor);
});

// Each selector in the stack maintains its own cache
getBooksForMultipleAuthors(state, [1, 2, 3]);
getBooksForMultipleAuthors(state, [4, 5, 6]);
// This hits the cache
// This reuses the cached values from the earlier calls
getBooksForMultipleAuthors(state, [1, 2, 3, 4, 5, 6]);
```

Expand Down Expand Up @@ -103,15 +105,14 @@ const getRawList = createDynamicSelector((getState, { listId }) => {
});

const getSortedList = createDynamicSelector((getState, { listId, sortField }) => {
// `state` is not pased when one selector calls another
// `state` is automatically pased through when one selector calls another
const rawList = getRawList({ listId });
if (rawList && sortField) {
return sortBy(rawList, sortField);
}
return rawList;
});

// `state` is passed in when you call the outermost selector
getSortedList(state, { listId: 123, sortField: 'title' });
```

Expand Down
116 changes: 84 additions & 32 deletions packages/core/docs/comparison-with-reselect.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,104 @@
# Comparison with other selector libraries
# Selector comparison

This shows several different ways to write [Reselect's example selector](https://github.com/reduxjs/reselect#example),
which filters `state.todos` based on a value in `state.visibilityFilter`.
This shows several different ways to write a simple selector that filters a list of books (`state.books`) based on
a author (`state.authorFilter`).

#### Plain, unmemoized function
## 1. Plain, unmemoized function

State values are passed to a normal function. This is straightforward to write, but it will repeat the work every
single time your state updates, with potentially bad performance.
The simplest implementation is to use no selector at all. This is straightforward to write, but it will repeat the work
every time your state updates -- and because array's `filter()` returns a new array every time it runs, this will always
trigger a rerender.

```javascript
const getVisibleTodos = (todos, filter) => {
// Snipped for brevity: filter `todos` by `filter`
const getBooksForAuthor = (books, authorFilter) => {
return books.filter((book) => book.author === authorFilter);
};

const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter),
};
};
useSelector((state) => getBooksForAuthor(state.books, state.authorFilter));
```

#### Using Reselect
## 2. Reselect selectors

Reselect offers a standard way to memoize the operation, building up a dependency tree of functions.

Each state value is wrapped in an accessor function, and `getVisibleTodos` is set to depend on those functions. It will
rerun only when one of the functions returns a new value. The selector's dependencies must be listed ahead of time.
Each state value is wrapped in an accessor function, and `getBooksForAuthor` is set to depend on those functions.
It will rerun only when one of the functions returns a new value. The selector's dependencies must be registered at
creation time.

```javascript
const getVisibilityFilter = (state) => state.visibilityFilter;
const getTodos = (state) => state.todos;

const getVisibleTodos = createSelector(
[getVisibilityFilter, getTodos],
(visibilityFilter, todos) => {
// Snipped for brevity: filter `todos` by `filter`
},
);
const getBooks = (state) => state.books;
const getAuthorFilter = (state) => state.authorFilter;

const getBooksForAuthor = createSelector([getBooks, getAuthorFilter], (books, authorFilter) => {
return books.filter((book) => book.author === authorFilter);
});
```

#### Using Dynamic Selectors
## 3. Dynamic Selector

`getVisibleTodos` can retrieve values directly from the state, or from other dynamic selectors. It will rerun only when
one of the functions returns a new value. You can retrieve values dynamically, or change the `getState` calls from run to run.
`getBooksForAuthor` can retrieve values directly from the state, or from other dynamic selectors. It will rerun only
when
one of the functions returns a new value. For simple cases, this ultimately works the same as a Reselect selector --
just with less code and fewer functions.

```javascript
const getVisibleTodos = createDynamicSelector((getState) => {
const visibilityFilter = getState('visibilityFilter');
const todos = getState('todos');
const getBooksForAuthor = createDynamicSelector((getState) => {
const books = getState('books');
const authorFilter = getState('authorFilter');

// Snipped for brevity: filter `todos` by `filter`
return books.filter((book) => book.author === authorFilter);
});
```

### More complex cases

#### Conditional dependency

With a dynamic selector you can retrieve values dynamically, change the `getState` calls from run to run, and generally
be flexible in ways that upfront selector registration doesn't allow.

Here's an example which allows the author to be overridden by the caller. Results are memoized independently by params,
so this will remain cached even when `state.authorFilter` changes: `state.authorFilter` only gets marked as a
dependency if it's actually used.

```javascript
const getBooksForAuthor = createDynamicSelector((getState, authorFilterOverride) => {
const books = getState('books');
const authorFilter = authorFilterOverride || getState('authorFilter');

return books.filter((book) => book.author === authorFilter);
});
```

#### Loops

You can build higher-level selectors on top of simpler selectors, without having to rewrite any accessors or other
logic.

In this example, `authorFilter` can be a single author or a list of authors. The caching all works as before, so
if one of the authors in the list already has a list of books cached, it will not be reprocessed.

```javascript
const getBooksForAuthor = createDynamicSelector((getState, authorFilterOverride) => {
const books = getState('books');
const authorFilter = authorFilterOverride || getState('authorFilter');

if (Array.isArray(authorFilter)) {
// Accumulate books for each author, and combine them into a list of lists
return authorFilter.map(
// Recurse!
(authorFilter) => getBooksForAuthor(authorFilter),
);
} else {
return books.filter((book) => book.author === authorFilter);
}
});
```

The above solution is `O(n^2)` for a large number of authors: in practice you probably don't want to loop over
the `books` multiple times.

With traditional selectors, refactoring this to loop over `books` only once would require rewriting the entire selector.
Instead, you could split the algorithm inside the selector: use `getBooksForAuthor` as before if the list is small,
or for any authors who already have a cached result (see the [API docs](../README.md#additional-selector-properties)
for `.hasCachedResult()`), and then process the remainder in a single loop.
25 changes: 12 additions & 13 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,44 +56,43 @@
"sideEffects": false,
"scripts": {
"____ HOOKS _________________________________________________________": "",
"prepare": "pnpm run build",
"prepack": "pnpm run clean && pnpm run dev:readonly",
"____ MAIN __________________________________________________________": "",
"build": "pnpm run build:clean && pnpm run build:main && pnpm run build:types",
"test": "pnpm run test:coverage",
"prepare": "pnpm run build:main",
"prepack": "pnpm run clean && pnpm run build",
"____ INTEGRATION ___________________________________________________": "",
"clean": "pnpm run build:clean && pnpm run test:clean && rimraf ./node_modules/.cache *.log",
"all": "pnpm run format && pnpm run types && pnpm run lint:fix && pnpm run test:coverage && pnpm run build",
"all:readonly": "pnpm run format:verify && pnpm run types && pnpm run lint && pnpm run test:quick",
"all:quick": "pnpm run format && pnpm run types && pnpm run lint:fix",
"all:ci": "pnpm run format:verify && pnpm run types && pnpm run lint && pnpm run test:ci && pnpm run build",
"____ INDIVIDUAL COMMANDS ___________________________________________": "",
"build": "pnpm run build:main && pnpm run build:legacytypes",
"build:clean": "rimraf ./dist ./legacy-types",
"build:main": "tsup ./src/index.ts ./src/index.devOnly.ts --format esm,cjs",
"build:types": "tsc --declaration --emitDeclarationOnly && pnpm run build:types:3.5 && pnpm run build:types:4.0 && pnpm run build:types:4.5",
"build:types:3.5": "downlevel-dts ./dist ./legacy-types/ts3.5 --to=3.5",
"build:types:4.0": "downlevel-dts ./dist ./legacy-types/ts4.0 --to=4.0",
"build:types:4.5": "downlevel-dts ./dist ./legacy-types/ts4.5 --to=4.5",
"build:watch": "tsup ./src/index.ts ./src/index.devOnly.ts --format esm --watch",
"build:main": "pnpm run build:clean && tsup src/index.ts ./src/index.devOnly.ts --format esm,cjs && tsc -p ./tsconfig.build.json --declaration --emitDeclarationOnly",
"build:legacytypes": "pnpm run build:legacytypes:3.5 && pnpm run build:legacytypes:4.0 && pnpm run build:legacytypes:4.5",
"build:legacytypes:3.5": "downlevel-dts ./dist ./legacy-types/ts3.5 --to=3.5",
"build:legacytypes:4.0": "downlevel-dts ./dist ./legacy-types/ts4.0 --to=4.0",
"build:legacytypes:4.5": "downlevel-dts ./dist ./legacy-types/ts4.5 --to=4.5",
"build:watch": "pnpm run build:clean && tsup src/index.ts ./src/index.devOnly.ts --format esm,cjs --watch",
"format": "prettier --write .",
"format:verify": "prettier --list-different .",
"lint": "eslint . --max-warnings 0",
"lint:fix": "eslint . --max-warnings 0 --fix",
"release:changelog": "standard-version --skip.commit --skip.tag --release-as ",
"test": "pnpm run test:coverage",
"test:clean": "rimraf ./coverage",
"test:ci": "pnpm run test:clean && vitest run --coverage",
"test:coverage": "pnpm run test:clean && vitest run --coverage",
"test:quick": "pnpm run test:clean && vitest run --coverage=false",
"test:watch": "pnpm run test:clean && vitest watch --coverage=false",
"test:watchcoverage": "pnpm run test:clean && vitest watch --coverage",
"types": "tsc --noEmit"
"types": "tsc -p ./tsconfig.json --noEmit"
},
"dependencies": {
"lodash-es": "^4.17.21",
"shallowequal": "^1.1.0"
},
"devDependencies": {
"@types/lodash-es": "4.17.6",
"@types/node": "18.7.13",
"@types/shallowequal": "1.1.1"
},
"typesVersions123": {
Expand Down
8 changes: 8 additions & 0 deletions packages/core/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig-base.build.json",
"compilerOptions": {
"outDir": "dist",
"declarationDir": "dist"
},
"include": ["src"]
}

0 comments on commit 1a1ffe4

Please sign in to comment.