Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^12.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/invariant": "^2.2.35",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
Expand Down
114 changes: 114 additions & 0 deletions packages/react/src/__tests__/useList-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @jest-environment jsdom
*/

'use strict';

let React;
let ReactDOMClient;
let act;
let useList;

describe('useList', () => {
beforeEach(() => {
jest.resetModules();

React = require('react');
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;

useList = require('../useList').default;
});

function renderHook(hook) {
const result = {current: null};
let rerender;
function TestComponent() {
const [, setTick] = React.useState(0);
rerender = () => setTick(t => t + 1);
result.current = hook();
return null;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const {flushSync} = require('react-dom');
flushSync(() => {
root.render(React.createElement(TestComponent));
});
return {result, rerender};
}

it('should initialize with provided list', () => {
const {result} = renderHook(() => useList([1, 2, 3]));
expect(result.current.list).toEqual([1, 2, 3]);
});

it('should initialize with empty list if none provided', () => {
const {result} = renderHook(() => useList());
expect(result.current.list).toEqual([]);
});

it('should set list using setList', async () => {
const {result, rerender} = renderHook(() => useList([1, 2, 3]));
await act(() => {
result.current.setList([4, 5, 6]);
rerender();
});
expect(result.current.list).toEqual([4, 5, 6]);
});

it('should remove items using predicate', async () => {
const {result, rerender} = renderHook(() => useList([1, 2, 3, 4]));
expect(result.current).not.toBe(null);
await act(() => {
result.current.remove(x => x % 2 === 0);
rerender();
});
expect(result.current.list).toEqual([1, 3]);
});

it('should not remove any items if predicate matches none', async () => {
const {result, rerender} = renderHook(() => useList([1, 2, 3]));
await act(() => {
result.current.remove(x => x > 10);
rerender();
});
expect(result.current.list).toEqual([1, 2, 3]);
});

it('should remove all items if predicate matches all', async () => {
const {result, rerender} = renderHook(() => useList([1, 2, 3]));
await act(() => {
result.current.remove(() => true);
rerender();
});
expect(result.current.list).toEqual([]);
});

it('should remove item at given index', async () => {
const {result, rerender} = renderHook(() => useList(['a', 'b', 'c']));
await act(() => {
result.current.removeAt(1);
rerender();
});
expect(result.current.list).toEqual(['a', 'c']);
});

it('should do nothing if removeAt is called with invalid index', async () => {
const {result, rerender} = renderHook(() => useList(['a', 'b', 'c']));
await act(() => {
result.current.removeAt(10);
rerender();
});
expect(result.current.list).toEqual(['a', 'b', 'c']);
});

it('should handle removeAt on empty list', async () => {
const {result, rerender} = renderHook(() => useList([]));
await act(() => {
result.current.removeAt(0);
rerender();
});
expect(result.current.list).toEqual([]);
});
});
24 changes: 24 additions & 0 deletions packages/react/src/useList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

import {useState, useCallback} from 'react';

export default function useList(initialList = []) {
const [list, setList] = useState(initialList);

const remove = useCallback(predicate => {
setList(prev => prev.filter(item => !predicate(item)));
}, []);

const removeAt = useCallback(index => {
setList(prev => prev.filter((_, i) => i !== index));
}, []);

return {list, setList, remove, removeAt};
}
21 changes: 21 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2442,6 +2442,11 @@
dependencies:
regenerator-runtime "^0.13.4"

"@babel/runtime@^7.12.5":
version "7.29.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e"
integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==

"@babel/template@^7.10.4":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
Expand Down Expand Up @@ -3809,6 +3814,14 @@
dependencies:
defer-to-connect "^2.0.1"

"@testing-library/react-hooks@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12"
integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==
dependencies:
"@babel/runtime" "^7.12.5"
react-error-boundary "^3.1.0"

"@tokenizer/token@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.1.tgz#f0d92c12f87079ddfd1b29f614758b9696bc29e3"
Expand Down Expand Up @@ -8167,6 +8180,7 @@ eslint-plugin-no-unsanitized@4.0.2:

"eslint-plugin-react-internal@link:./scripts/eslint-rules":
version "0.0.0"
uid ""

eslint-plugin-react@^6.7.1:
version "6.10.3"
Expand Down Expand Up @@ -14633,6 +14647,13 @@ rc@1.2.8, rc@^1.2.8:
object-assign "^4.1.1"
scheduler "^0.20.2"

react-error-boundary@^3.1.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
dependencies:
"@babel/runtime" "^7.12.5"

react-is@^16.8.1, react-is@^18.0.0, "react-is@npm:react-is":
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
Expand Down