Skip to content

Commit 12cf44e

Browse files
committed
Merge branch 'master' of https://github.com/reduxjs/redux-toolkit into configs
2 parents 49868f1 + 9964cdd commit 12cf44e

File tree

10 files changed

+2385
-1796
lines changed

10 files changed

+2385
-1796
lines changed

examples/publish-ci/react-native/App.test.tsx

+210
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
import type { Action, Dispatch } from "@reduxjs/toolkit"
2+
import { configureStore } from "@reduxjs/toolkit"
13
import { screen, waitFor } from "@testing-library/react-native"
4+
import { Component, PureComponent, type PropsWithChildren } from "react"
5+
import type { TextStyle } from "react-native"
6+
import { Button, Text, View } from "react-native"
7+
import { connect, Provider } from "react-redux"
28
import { App } from "./App"
39
import { renderWithProviders } from "./src/utils/test-utils"
410

@@ -103,3 +109,207 @@ test("Add If Odd should work as expected", async () => {
103109
await user.press(screen.getByText("Add If Odd"))
104110
expect(screen.getByLabelText("Count")).toHaveTextContent("4")
105111
})
112+
113+
test("React-Redux issue #2150: Nested component updates should be properly batched when using connect", async () => {
114+
// Original Issue: https://github.com/reduxjs/react-redux/issues/2150
115+
// Solution: https://github.com/reduxjs/react-redux/pull/2156
116+
117+
// Actions
118+
const ADD = "ADD"
119+
const DATE = "DATE"
120+
121+
// Action types
122+
interface AddAction extends Action<string> {}
123+
interface DateAction extends Action<string> {
124+
payload?: { date: number }
125+
}
126+
127+
// Reducer states
128+
interface DateState {
129+
date: number | null
130+
}
131+
132+
interface CounterState {
133+
count: number
134+
}
135+
136+
// Reducers
137+
const dateReducer = (
138+
state: DateState = { date: null },
139+
action: DateAction,
140+
) => {
141+
switch (action.type) {
142+
case DATE:
143+
return {
144+
...state,
145+
date: action.payload?.date ?? null,
146+
}
147+
default:
148+
return state
149+
}
150+
}
151+
152+
const counterReducer = (
153+
state: CounterState = { count: 0 },
154+
action: AddAction,
155+
) => {
156+
switch (action.type) {
157+
case ADD:
158+
return {
159+
...state,
160+
count: state.count + 1,
161+
}
162+
default:
163+
return state
164+
}
165+
}
166+
167+
// Store
168+
const store = configureStore({
169+
reducer: {
170+
counter: counterReducer,
171+
dates: dateReducer,
172+
},
173+
})
174+
175+
// ======== COMPONENTS =========
176+
interface CounterProps {
177+
count?: number
178+
date?: number | null
179+
dispatch: Dispatch<AddAction | DateAction>
180+
testID?: string
181+
}
182+
183+
class CounterRaw extends PureComponent<CounterProps> {
184+
handleIncrement = () => {
185+
this.props.dispatch({ type: ADD })
186+
}
187+
188+
handleDate = () => {
189+
this.props.dispatch({ type: DATE, payload: { date: Date.now() } })
190+
}
191+
192+
render() {
193+
return (
194+
<View style={{ paddingVertical: 20 }}>
195+
<Text testID={`${this.props.testID}-child`}>
196+
Counter Value: {this.props.count}
197+
</Text>
198+
<Text>date Value: {this.props.date}</Text>
199+
</View>
200+
)
201+
}
202+
}
203+
204+
class ButtonsRaw extends PureComponent<CounterProps> {
205+
handleIncrement = () => {
206+
this.props.dispatch({ type: ADD })
207+
}
208+
209+
handleDate = () => {
210+
this.props.dispatch({ type: DATE, payload: { date: Date.now() } })
211+
}
212+
213+
render() {
214+
return (
215+
<View>
216+
<Button title="Update Date" onPress={this.handleDate} />
217+
<View style={{ height: 20 }} />
218+
<Button title="Increment Counter" onPress={this.handleIncrement} />
219+
</View>
220+
)
221+
}
222+
}
223+
224+
const mapStateToProps = (state: {
225+
counter: CounterState
226+
dates: DateState
227+
}) => {
228+
return { count: state.counter.count, date: state.dates.date }
229+
}
230+
231+
const mapDispatchToProps = (dispatch: Dispatch<AddAction | DateAction>) => ({
232+
dispatch,
233+
})
234+
235+
const Buttons = connect(null, mapDispatchToProps)(ButtonsRaw)
236+
const Counter = connect(mapStateToProps, mapDispatchToProps)(CounterRaw)
237+
238+
class Container extends PureComponent<PropsWithChildren> {
239+
render() {
240+
return this.props.children
241+
}
242+
}
243+
244+
const mapStateToPropsBreaking = (_state: any) => ({})
245+
246+
const ContainerBad = connect(mapStateToPropsBreaking, null)(Container)
247+
248+
const mapStateToPropsNonBlocking1 = (state: { counter: CounterState }) => ({
249+
count: state.counter.count,
250+
})
251+
252+
const ContainerNonBlocking1 = connect(
253+
mapStateToPropsNonBlocking1,
254+
null,
255+
)(Container)
256+
257+
const mapStateToPropsNonBlocking2 = (state: any) => ({ state })
258+
259+
const ContainerNonBlocking2 = connect(
260+
mapStateToPropsNonBlocking2,
261+
null,
262+
)(Container)
263+
264+
class MainApp extends Component {
265+
render() {
266+
const $H1: TextStyle = { fontSize: 20 }
267+
return (
268+
<Provider store={store}>
269+
<Buttons />
270+
<Text style={$H1}>=Expected=</Text>
271+
<View>
272+
<Text>
273+
I don't have a parent blocking state updates so I should behave as
274+
expected
275+
</Text>
276+
<Counter />
277+
</View>
278+
279+
<Text style={$H1}>=Undesired behavior with react-redux 9.x=</Text>
280+
<ContainerBad>
281+
<Text>All redux state updates blocked</Text>
282+
<Counter testID="undesired" />
283+
</ContainerBad>
284+
285+
<Text style={$H1}>=Partially working in 9.x=</Text>
286+
<ContainerNonBlocking1>
287+
<Text>
288+
I'm inconsistent, if date updates first I don't see it, but once
289+
count updates I rerender with count or date changes
290+
</Text>
291+
<Counter testID="inconsistent" />
292+
</ContainerNonBlocking1>
293+
294+
<Text style={$H1}>=Poor workaround for 9.x?=</Text>
295+
<ContainerNonBlocking2>
296+
<Text>I see all state changes</Text>
297+
<Counter />
298+
</ContainerNonBlocking2>
299+
</Provider>
300+
)
301+
}
302+
}
303+
304+
const { user, getByTestId, getByText } = renderWithProviders(<MainApp />)
305+
306+
expect(getByTestId("undesired-child")).toHaveTextContent("Counter Value: 0")
307+
308+
await user.press(getByText("Increment Counter"))
309+
310+
expect(getByTestId("inconsistent-child")).toHaveTextContent(
311+
"Counter Value: 1",
312+
)
313+
314+
expect(getByTestId("undesired-child")).toHaveTextContent("Counter Value: 1")
315+
})

examples/publish-ci/react-native/babel.config.cts

-7
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/** @import { ConfigFunction } from "@babel/core" */
2+
3+
/**
4+
* @satisfies {ConfigFunction}
5+
*/
6+
const config = api => {
7+
api.cache.using(() => process.env.NODE_ENV)
8+
9+
return {
10+
presets: [["module:@react-native/babel-preset"]],
11+
}
12+
}
13+
14+
module.exports = config

examples/publish-ci/react-native/jest.config.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,26 @@ import type { Config } from "jest"
22

33
const config: Config = {
44
preset: "react-native",
5-
testEnvironment: "node",
5+
verbose: true,
6+
/**
7+
* Without this we will get the following error:
8+
* `SyntaxError: Cannot use import statement outside a module`
9+
*/
10+
transformIgnorePatterns: [
11+
"node_modules/(?!((jest-)?react-native|...|react-redux))",
12+
],
13+
/**
14+
* React Native's `jest` preset includes a
15+
* [polyfill for `window`](https://github.com/facebook/react-native/blob/acb634bc9662c1103bc7c8ca83cfdc62516d0060/packages/react-native/jest/setup.js#L61-L66).
16+
* This polyfill causes React-Redux to use `useEffect`
17+
* instead of `useLayoutEffect` for the `useIsomorphicLayoutEffect` hook.
18+
* As a result, nested component updates may not be properly batched
19+
* when using the `connect` API, leading to potential issues.
20+
*/
21+
globals: {
22+
window: undefined,
23+
navigator: { product: "ReactNative" },
24+
},
625
setupFilesAfterEnv: ["<rootDir>/jest-setup.ts"],
726
fakeTimers: { enableGlobally: true },
827
}

examples/publish-ci/react-native/package.json

+21-17
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,38 @@
1414
"type-check": "tsc --noEmit"
1515
},
1616
"dependencies": {
17-
"@reduxjs/toolkit": "^2.0.1",
17+
"@reduxjs/toolkit": "^2.2.7",
1818
"react": "18.2.0",
19-
"react-native": "^0.73.2",
20-
"react-redux": "^9.1.0"
19+
"react-native": "^0.74.5",
20+
"react-redux": "^9.1.2"
2121
},
2222
"devDependencies": {
23-
"@babel/core": "^7.23.7",
24-
"@babel/preset-env": "^7.23.8",
25-
"@babel/runtime": "^7.23.8",
26-
"@react-native/babel-preset": "^0.73.19",
27-
"@react-native/eslint-config": "^0.74.0",
28-
"@react-native/metro-config": "^0.73.3",
29-
"@react-native/typescript-config": "^0.74.0",
30-
"@testing-library/react-native": "^12.4.3",
31-
"@types/jest": "^29.5.11",
23+
"@babel/core": "^7.25.2",
24+
"@babel/preset-env": "^7.25.3",
25+
"@babel/preset-typescript": "^7.24.7",
26+
"@babel/runtime": "^7.25.0",
27+
"@react-native/babel-preset": "^0.74.87",
28+
"@react-native/eslint-config": "^0.74.87",
29+
"@react-native/metro-config": "^0.74.87",
30+
"@react-native/typescript-config": "^0.74.87",
31+
"@testing-library/react-native": "^12.5.2",
32+
"@types/babel__core": "^7.20.5",
33+
"@types/eslint": "^9",
34+
"@types/jest": "^29.5.12",
35+
"@types/node": "^22.1.0",
3236
"@types/react": "^18.2.47",
3337
"@types/react-test-renderer": "^18.0.7",
34-
"@typescript-eslint/eslint-plugin": "^6.18.1",
35-
"@typescript-eslint/parser": "^6.18.1",
38+
"@typescript-eslint/eslint-plugin": "^8.0.1",
39+
"@typescript-eslint/parser": "^8.0.1",
3640
"babel-jest": "^29.7.0",
37-
"eslint": "^8.56.0",
41+
"eslint": "^9.8.0",
3842
"eslint-config-prettier": "^9.1.0",
39-
"eslint-plugin-prettier": "^5.1.3",
43+
"eslint-plugin-prettier": "^5.2.1",
4044
"jest": "^29.7.0",
4145
"prettier": "^3.3.3",
4246
"react-test-renderer": "18.2.0",
4347
"ts-node": "^10.9.2",
44-
"typescript": "^5.3.3"
48+
"typescript": "^5.5.4"
4549
},
4650
"engines": {
4751
"node": ">=18"

0 commit comments

Comments
 (0)