, a very interesting , has just reached at . Records & Tuples proposal stage 2 TC39 They bring to JavaScript. deeply immutable data structures But don't overlook their , that are interesting for . equality properties VERY React A whole category of are related to : React bugs unstable object identities : re-renders that could be avoided Performance : useless effect re-executions, infinite loops Behavior : unability to express when a stable object identity matters API surface I will explain the basics of , and how they can solve . Records & Tuples real world React issues Records & Tuples 101 This article is about Records & Tuples . I'll only cover the basics here. for React They look like regular , with a prefix. Objects and Arrays # record = #{ : , : }; record.a; updatedRecord = #{...record, : }; tuple = #[ , , , , ]; tuple[ ]; filteredTuple = tuple.filter( num > ) const a 1 b 2 // 1 const b 3 // #{a: 1, b: 3}; const 1 5 2 3 4 1 // 5 const => num 2 // #[5, 3, 4]; They are by default. deeply immutable record = #{ : , : }; record.b = ; const a 1 b 2 3 // throws TypeError They can be seen as , and can be compared by value. "compound primitives" : two deeply equal records will return true with ===. VERY IMPORTANT ALWAYS { : , : [ , ]} === { : , : [ , ]} #{ : , : #[ , ]} === #{ : , : #[ , ]} a 1 b 3 4 a 1 b 3 4 // with objects => false a 1 b 3 4 a 1 b 3 4 // with records => true We can somehow consider that the identity of a Record is its actual value, like any regular JS primitive. This property has , as we will see. deep implications for React They are interoperable with JSON: record = .parseImmutable( ); .stringify(record); const JSON '{a: 1, b: [2, 3]}' // #{a: 1, b: #[2, 3]} JSON // '{a: 1, b: [2, 3]}' They can only contain other records and tuples, or primitive values. record1 = #{ : { : }, }; record2 = #{ : (), }; record3 = #{ : MyClass(), }; record4 = #{ : { alert( ); }, }; const a regular 'object' // throws TypeError, because a record can't contain an object const b new Date // throws TypeError, because a record can't contain a Date const c new // throws TypeError, because a record can't contain a class const d ( ) function 'forbidden' // throws TypeError, because a record can't contain a function : you may be able to add such mutable values to a Record, by using Symbols as WeakMap keys ( ), and the symbols in records. Note separate proposal reference Want more? Read the directly, or this from Axel Rauschmayer. proposal article Records & Tuples for React React developers are now used to . immutability Every time you update some piece of state in an immutable way, you create . new object identities Unfortunately, this immutability model has introduced a whole new class of bugs, and performance issues in React applications. Sometimes, a component works correctly and in a performant way, as most as they can over time. only under the assumption that props preserve identities I like to think about Records & Tuples as a convenient way to . make object identities more "stable" Let's see how this proposal will with practical use cases. impact your React code : there is a , that can run React. Note Records & Tuples playground Immutability Enforcing immutability can be achieved with recursive Object.freeze() calls. But in practice, we often use the immutability model without enforcing it too strictly, as it's not convenient to apply Object.freeze() after each update. Yet, mutating the React state directly is a common mistake for new React developers. The Records & Tuples proposal will , and prevent common state mutation mistakes: enforce immutability Hello = { profile.name = ; ; }; { [profile, setProfile] = React.useState(#{ : , }); profile.name = ; const ( ) => { profile } // prop mutation: throws TypeError 'Sebastien updated' return Hello {profile.name} < > p </ > p ( ) function App const name 'Sebastien' // state mutation: throws TypeError 'Sebastien updated' return ; } < = /> Hello profile {profile} Immutable updates There are to perform immutable state updates in React: vanilla JS, Lodash set, ImmerJS, ImmutableJS... many ways Records & Tuples support the same kind of immutable update patterns that you use with ES6 Objects and Arrays: initialState = #{ : #{ : , : } company: #{ : , } }; updatedState = { ...initialState, : { ...initialState.company, : , }, }; const user firstName "Sebastien" lastName "Lorber" name "Lambda Scale" const company name 'Freelance' So far, has won the immutable updates battle, due to its simplicity to handle nested attributes, and interoperability with regular JS code. ImmerJS It is not clear how Immer could work with Records & Tuples yet, but it's something the proposal authors are exploring. Michael Weststrate himself has that a could : highlighted separate but related proposal make ImmerJS unnecessary for Records & Tuples initialState = #{ : #[ #{ : , : }, #{ : , : }, #{ : , : }, ], : #{ : , }, }; updatedState = #{ ...initialState, counters[ ].value: , counters[ ].value: , metadata.lastUpdate: , }; const counters name "Counter 1" value 1 name "Counter 2" value 0 name "Counter 3" value 123 metadata lastUpdate 1584382969000 // Vanilla JS updates // using deep-path-properties-for-record proposal const 0 2 1 1 1584383011300 useMemo In addition to memoizing expensive computations, is also useful to , that might . useMemo() avoid creating new object identities trigger useless computations, re-renders, or effects executions deeper in the tree Let's consider the following use-case: you have an UI with multiple filters, and want to fetch some data from the backend. Existing React code-bases might contain code such as: apiFilters = useMemo( ({ userFilter, companyFilter }), [userFilter, companyFilter], ); { apiData, loading } = useApiData(apiFilters); // Don't change apiFilters object identity, // unless one of the filter changes // Not doing this is likely to trigger a new fetch // on each render const => () const With Records & Tuples, this simply becomes: {apiData,loading} = useApiData(#{ userFilter, companyFilter }) const useEffect Let's continue with our api filters use-case: apiFilters = { userFilter, companyFilter }; useEffect( { fetchApiData(apiFilters).then(setApiDataInState); }, [apiFilters]); const => () Unfortunately, the fetch effect gets , because the identity of the object changes every time this component re-renders. will trigger a re-render, and you will end up with an infinite fetch/render loop. re-executed apiFilters setApiDataInState This mistake is so common across React developers that there are thousand of Google search results for . useEffect + "infinite loop" even created a to break infinite loops in development. Kent C Dodds tool Very common solution: create directly in the effect's callback apiFilters useEffect( { apiFilters = { userFilter, companyFilter }; fetchApiData(apiFilters).then(setApiDataInState); }, [userFilter, companyFilter]); => () const Another creative solution (not very performant, found on ): Twitter apiFiltersString = .stringify({ userFilter, companyFilter, }); useEffect( { fetchApiData( .parse(apiFiltersString)).then( setApiDataInState, ); }, [apiFiltersString]); const JSON => () JSON The one I like the most: apiFilters = useMemo( ({ userFilter, companyFilter }), [userFilter, companyFilter], ); useEffect( { fetchApiData(apiFilters).then(setApiDataInState); }, [apiFilters]); // We already saw this somewhere, right? :p const => () => () There are many fancy ways to solve this problem, but they all tend to , as the number of filters increase. become annoying (from ) is likely the less annoying, but running deep equality on every re-render has a cost I'd prefer not pay. use-deep-compare-effect Kent C Dodds They are much than their Records & Tuples counterpart: more verbose and less idiomatic apiFilters = #{ userFilter, companyFilter }; useEffect( { fetchApiData(apiFilters).then(setApiDataInState); }, [apiFilters]); const => () Props and React.memo Preserving object identities in props is also very useful for React performances. Another very common performance mistake: create new objects identities in render. Parent = { useRerenderEverySeconds(); ( <div>{expensiveRender(someData)}</div> const => () return ); }; const ExpensiveChild = React.memo(({ someData }) => { return < // " " = ' ', ' ' }} /> ExpensiveChild someData props object is created on the fly someData {{ attr1: abc attr2: def ; }); Most of the time, this is not a problem, and React is fast enough. But sometimes you are looking to optimize your app, and this new object creation makes the useless. Worst, it actually (as it now has to run an additional shallow equality check, always returning false). React.memo() makes your application a little bit slower Another pattern I often see in client code-bases: currentUser = { : }; currentCompany = { : }; AppProvider = { useRerenderEverySeconds(); ( const name 'Sebastien' const name 'Lambda Scale' const => () return ); }; < // " " = , }} /> MyAppContext.Provider the value prop object is created on the fly value {{ currentUser currentCompany Despite the fact that or , your context value changes every time this provider re-renders, which trigger re-renders of all context subscribers. currentUser currentCompany never gets updated All these issues can be solved with memoization: someData = useMemo( ({ : , : }), [], ); const => () attr1 'abc' attr2 'def' ; < = /> ExpensiveChild someData {someData} contextValue = useMemo( ({ currentUser, currentCompany }), [currentUser, currentCompany], ); const => () ; < = /> MyAppContext.Provider value {contextValue} With Records & Tuples, it is : idiomatic to write performant code <ExpensiveChild someData={#{ : , : }} />; attr1 'abc' attr2 'def' <MyAppContext.Provider value={#{ currentUser, currentCompany }} />; Fetching and re-fetching There are many ways to fetch data in React: useEffect, HOC, Render props, Redux, SWR, React-Query, Apollo, Relay, Urql, ... Most often, we hit the backend with a request, and get some JSON data back. To illustrate this section, I will use , my own very simple fetching library, but this applies to other libraries as well. react-async-hook Let's consider a classic async function to get some API data: fetchUserAndCompany = () => { response = fetch( , ); response.json(); }; const async const await `https://myBackend.com/userAndCompany` return This app fetches the data, and ensure this data stays "fresh" (non-stale) over time: App = { { result, refetch } = useAsync( fetchUserAndCompany, [], ); useInterval(refetch, ); useOnReconnect(refetch); useOnNavigate(refetch); (!result) { ; } ( <User user={result.user} /> <Company company={result.company} /> </div> ); }; const User = React.memo(({ user }) => { return <div>{user.name}</div>; }); const Company = React.memo(({ company }) => { return <div>{company.name}</div>; }); const ( ) => { id } const // We try very hard to not display stale data to the user! 10000 if return null return < > div Problem: you have used React.memo for performance reasons, but every time the re-fetch happens, you end up with a new JS object, with a , and , despite the fetched data being the (deeply equal payloads). new identity everything re-renders same as before Let's imagine this scenario: you use the "Stale-While-Revalidate" pattern (show cached/stale data first, then refresh data in the background) your page is complex, render intensive, with lots of backend data being displayed You navigate to a page, that is already expensive to render the first time (with cached data). One second later, the refreshed data comes back. Despite being deeply equal to the cached data, everything re-renders again. Without and time slicing, some users may even notice for a few hundred milliseconds. Concurrent Mode their UI freezing Now, let's convert the fetch function to return a Record instead: fetchUserAndCompany = () => { response = fetch( , ); .parseImmutable( response.text()); }; const async const await `https://myBackend.com/userAndCompany` return JSON await By chance, JSON is compatible with Records & Tuples, and you should be able to convert any backend response to a Record, with . JSON.parseImmutable : , one of the proposal authors, is pushing for a new function. Note Robin Ricard response.immutableJson() With Records & Tuples, if the backend returns the same data, you at all! don't re-render anything Also, if only one part of the response has changed, the other nested objects of the response will still . This means that if only changed, the component will re-render, but not the component! keep their identity user.name User Company I let you imagine the performance impact of all this, considering patterns like "Stale-While-Revalidate" are becoming increasingly popular, and provided out of the box by libraries such as SWR, React-Query, Apollo, Relay... Reading query strings In search UIs, it's a good practice to . The user can then copy/paste the link to someone else, refresh the page, or bookmark it. preserve the state of filters in the querystring If you have 1 or 2 filters, that's simple, but as soon as your search UI becomes complex (10+ filters, ability to compose queries with AND/OR logic...), you'd better use a good abstraction to manage your querystring. I personally like : it's one of the few libraries that handle nested objects. qs queryStringObject = { : { : , }, : , }; queryString = qs.stringify(queryStringObject); queryStringObject2 = qs.parse(queryString); assert.deepEqual(queryStringObject, queryStringObject2); assert(queryStringObject !== queryStringObject2); const filters userName 'Sebastien' displayMode 'list' const const and are deeply equal, but they have not the same identity anymore, because qs.parse creates new objects. queryStringObject queryStringObject2 You can integrate the querystring parsing in a hook, and "stabilize" the querystring object with , or a lib such as . useMemo() use-memo-value useQueryStringObject = { { search } = useLocation(); useMemo( qs.parse(search), [search]); }; const => () // Provided by your routing library, like React-Router const return => () Now, imagine that deeper in the tree you have: { filters } = useQueryStringObject(); useEffect( { fetchUsers(filters).then(setUsers); }, [filters]); const => () This is a bit nasty here, but the same problem happens again and again. Despite the usage of , as an attempt to preserve identity, you will end up with unwanted calls. useMemo() queryStringObject fetchUsers When the user will update the (that should only change the rendering logic, not trigger a re-fetch), the querystring will change, leading to the querystring being parsed again, leading to a new object identity for the attribute, leading to the unwanted execution. displayMode filters useEffect Again, Records & Tuples would prevent such things to happen. parseQueryStringAsRecord = { queryStringObject = qs.parse(search); .parseImmutable( .stringify(queryStringObject), ); }; useQueryStringRecord = { { search } = useLocation(); useMemo( parseQueryStringAsRecord(search), [ search, ]); }; // This is a non-performant, but working solution. // Lib authors should provide a method such as qs.parseRecord(search) const ( ) => search const // Note: the Record(obj) conversion function is not recursive // There's a recursive conversion method here: // https://tc39.es/proposal-record-tuple/cookbook/index.html return JSON JSON const => () const return => () Now, even if the user updates the , the object will preserve its identity, and not trigger any useless re-fetch. displayMode filters : if the Records & Tuples proposal is accepted, libraries such as qs will likely provide a method. Note qs.parseRecord(search) Deeply equal JS transformations Imagine the following JS transformation in a component: AllUsers = [ { : , : }, { : , : }, ]; Parent = { userIdsToHide = useUserIdsToHide(); users = AllUsers.filter( !userIdsToHide.includes(user.id), ); <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> const id 1 name 'Sebastien' id 2 name 'John' const => () const const ( ) => user return ; }; const UserList = React.memo(({ users }) => ( < = /> UserList users {users} )); Every time the component re-renders, the component re-render as well, because filter will always return a . Parent UserList new array instance This is the case even if is empty, and identity being stable! In such case, the filter operation does not actually filter anything, it just , opting out of our optimizations. userIdsToHide AllUsers creates new useless array instances React.memo These kind of transformations are very common in React codebase, with operators such as or , in components, reducers, selectors, Redux... map filter Memoization can solve this, but it's more idiomatic with Records & Tuples: AllUsers = #[ #{ : , : }, #{ : , : }, ]; filteredUsers = AllUsers.filter( ); AllUsers === filteredUsers; const id 1 name 'Sebastien' id 2 name 'John' const => () true // true Records as React key Let's imagine you have a list of items to render: list = [ { : , : }, { : , : }, { : , : }, ]; const country 'FR' localPhoneNumber '111111' country 'FR' localPhoneNumber '222222' country 'US' localPhoneNumber '111111' What key would you use? Considering both the and are not in the list, you have 2 possible choices. country localPhoneNumber independently unique Array index key: <> {list.map( ( ( ) => item, index ))} < = ` ${ }`} = /> Item key { poormans_key_ index item {item} </> This always works, but is far from ideal, particularly if the items in the . list are reordered Composite key: <> {list.map( ( ( ) => item ))} < = `${ } ${ }`} = /> Item key { item.country _ item.localPhoneNumber item {item} </> This solution handle better the , but is only possible if we know for sure that the . list re-orderings couple / tuple is unique In such case, wouldn't it be to use directly? more convenient Records as the key list = #[ #{ : , : }, #{ : , : }, #{ : , : }, ]; <Item key={item} item={item} /> ))} </> const country 'FR' localPhoneNumber '111111' country 'FR' localPhoneNumber '222222' country 'US' localPhoneNumber '111111' {list.map((item) => ( <> This was by . suggested Morten Barklund Explicit API surface Let's consider this TypeScript component: UsersPageContent = ({ usersFilters, }: { : UsersFilters, }) => { [users, setUsers] = useState([]); useEffect( { fetchUsers(usersFilters).then(setUsers); }, [usersFilters]); const usersFilters const // poor-man's fetch => () return ; }; < = /> Users users {users} This code may or may not create an infinite loop, as we have seen already, depending on how stable the prop is. This creates an implicit API contract that should be documented and clearly understood by the implementor of the parent component, and despite using TypeScript, this is not reflected in the type-system. usersFilters The following will lead to an infinite loop, but TypeScript has no way to prevent it: <UsersPageContent usersFilters={{ nameFilter, ageFilter }} /> With Records & Tuples, we can tell TypeScript to expect a Record: UsersPageContent = ({ usersFilters, }: { : #{ : string, : string} }) => { [users, setUsers] = useState([]); useEffect( { fetchUsers(usersFilters).then(setUsers); }, [usersFilters]); const usersFilters nameFilter ageFilter const // poor-man's fetch => () return ; }; < = /> Users users {users} : the is my own invention: we don't know yet what will be the TypeScript syntax. Note #{nameFilter: string, ageFilter: string} TypeScript compilation will fail for: <UsersPageContent usersFilters={{ nameFilter, ageFilter }} /> While TypeScript would accept: <UsersPageContent usersFilters={#{ nameFilter, ageFilter }} /> With Records & Tuples, we can prevent this infinite loop at . compile time We have an way to tell the compiler that our (or relies on by-value comparisons). explicit implementation is object-identity sensitive : would not solve this: it only prevents mutations, but does not guarantee a stable identity. Note readonly Serialization guarantee You may want to ensure that developers on your team don't put unserializable things in global app state. This is important if you plan to send the state to the backend, or persist it locally in (or for React-Native users). localStorage AsyncStorage To ensure that, you just need to ensure that the root object is a record. This will guarantee that all the nested attributes are also primitives, including nested records and tuples. Here's an example integration with Redux, to ensure the Redux store keeps being serializable over time: (process.env.NODE_ENV === ) { ReduxStore.subscribe( { ( ReduxStore.getState() !== ) { ( + , ); } }); } if 'development' => () if typeof 'record' throw new Error "Don't put non-serializable things in the Redux store! " 'The root Redux state must be a record!' : this is not a perfect guarantee, because can be put in a Record, and is not serializable. Note Symbol CSS-in-JS performances Let's consider some CSS-in-JS from a popular lib, using the prop: css Component = ( ); const => () This has a hotpink background. < = ' ', }} > div css {{ backgroundColor: hotpink </ > div Your CSS-in-JS library receives a new CSS object on every re-render. On first render, it will hash this object as a unique class name, and insert the CSS. The style object has a different identity for each re-render, and the CSS-in-JS library have to . hash it again and again insertedClassNames = (); { className = computeStyleHash(styleObject); (!insertedClassNames.has(className)) { insertCSS(className, styleObject); insertedClassNames.add(className); } className; } const new Set ( ) function handleStyleObject styleObject // computeStyleHash re-executes every time const // only insert the css for this className once if return With Records & Tuples, the identity of such a style object is preserved over time. Component = ( ); const => () This has a hotpink background. < = ' ', }} > div css {#{ backgroundColor: hotpink </ > div Records & Tuples can be used as . This could make the implementation of your CSS-in-JS library faster: Map keys insertedStyleRecords = (); { className = insertedStyleRecords.get(styleRecord); (!className) { className = computeStyleHash(styleRecord); insertCSS(className, styleRecord); insertedStyleRecords.add(styleRecord, className); } className; } const new Map ( ) function handleStyleRecord styleRecord let if // computeStyleHash is only executed once! return We don't know yet about Records & Tuples (this will depend on browser vendor implementations), but I think it's safe to say it will be faster than creating the equivalent object, and then hashing it to a className. performances : some CSS-in-JS library with a good Babel plugin might be able to transform static style objects as constants at compilation time, but they will have a hard time doing so with dynamic styles. Note staticStyleObject = { : }; Component = ( ); const backgroundColor 'hotpink' const => () This has a hotpink background. < = > div css {staticStyleObject} </ > div Conclusion Many React performance and behavior issues are related to object identities. will ensure that object identities are out of the box, by providing some kind of , and help us solve these React problems more easily. Records & Tuples "more stable" "automatic memoization" Using TypeScript, we may be able to express better that your API surface is . object-identity sensitive I hope you are now as excited as I am by this proposal! Thank you for reading! Thanks , , , , for their work on this awesome proposal, and for reviewing my article. Robin Ricard Rick Button Daniel Ehrenberg Nicolò Ribaudo Rob Palmer If you like it, spread the word with a , or . Retweet Reddit HackerNews Browser code demos, or correct my post typos on the blog repo For more content like this, subscribe to and follow me on . my mailing list Twitter Previously published at https://sebastienlorber.com/records-and-tuples-for-react