Skip to content

Commit cd39b82

Browse files
committed
demonstrate react-redux issue 2150 in a brand new react-native application
reduxjs/react-redux#2150
1 parent 4a0b6ad commit cd39b82

File tree

1 file changed

+186
-104
lines changed

1 file changed

+186
-104
lines changed

App.tsx

Lines changed: 186 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,200 @@
11
/**
2-
* Sample React Native App
3-
* https://github.com/facebook/react-native
2+
* This app demonstrates undesired behavior in react-redux 9.x where mapStateToProps in a parent
3+
* component blocks state updates to child components.
44
*
5-
* @format
5+
* Essentially I have 2 reducers, one to update a date to the current timestamp and another to increment a counter.
6+
* I provide buttons to dispatch these actions.
7+
*
8+
* I then have two components each with their own mapStateToProps, one as a child of the other.
9+
* As of react-redux 9.x, the parent component's mapStateToProps blocks state updates to the child component if the child component's mapStateToProps isn't a subset of the parents.
10+
*
11+
* I could NOT reproduce on web with the same scenario https://codepen.io/steveng9/pen/QWPmLao
12+
*
13+
* This is related to https://github.com/reduxjs/react-redux/issues/2150
614
*/
7-
815
import React from 'react';
9-
import type {PropsWithChildren} from 'react';
10-
import {
11-
SafeAreaView,
12-
ScrollView,
13-
StatusBar,
14-
StyleSheet,
15-
Text,
16-
useColorScheme,
17-
View,
18-
} from 'react-native';
19-
20-
import {
21-
Colors,
22-
DebugInstructions,
23-
Header,
24-
LearnMoreLinks,
25-
ReloadInstructions,
26-
} from 'react-native/Libraries/NewAppScreen';
27-
28-
type SectionProps = PropsWithChildren<{
29-
title: string;
30-
}>;
31-
32-
function Section({children, title}: SectionProps): React.JSX.Element {
33-
const isDarkMode = useColorScheme() === 'dark';
34-
return (
35-
<View style={styles.sectionContainer}>
36-
<Text
37-
style={[
38-
styles.sectionTitle,
39-
{
40-
color: isDarkMode ? Colors.white : Colors.black,
41-
},
42-
]}>
43-
{title}
44-
</Text>
45-
<Text
46-
style={[
47-
styles.sectionDescription,
48-
{
49-
color: isDarkMode ? Colors.light : Colors.dark,
50-
},
51-
]}>
52-
{children}
53-
</Text>
54-
</View>
55-
);
16+
import {Button, Text, View, TextStyle} from 'react-native';
17+
import {Provider, connect, Dispatch} from 'react-redux';
18+
import {configureStore, Action} from '@reduxjs/toolkit';
19+
20+
// ========= REDUX SETUP =========
21+
22+
// Actions
23+
const ADD = 'ADD';
24+
const DATE = 'DATE';
25+
26+
// Action types
27+
interface AddAction extends Action<typeof ADD> {}
28+
interface DateAction extends Action<typeof DATE> {
29+
payload: {date: number};
30+
}
31+
32+
// Reducer states
33+
interface DateState {
34+
date: number | null;
35+
}
36+
37+
interface CounterState {
38+
count: number;
39+
}
40+
41+
// Reducers
42+
const dateReducer = (state: DateState = {date: null}, action: DateAction) => {
43+
switch (action.type) {
44+
case DATE:
45+
return {
46+
...state,
47+
date: action.payload.date,
48+
};
49+
default:
50+
return state;
51+
}
52+
};
53+
54+
const counterReducer = (
55+
state: CounterState = {count: 0},
56+
action: AddAction,
57+
) => {
58+
switch (action.type) {
59+
case ADD:
60+
return {
61+
...state,
62+
count: state.count + 1,
63+
};
64+
default:
65+
return state;
66+
}
67+
};
68+
69+
// Store
70+
const store = configureStore({
71+
reducer: {
72+
counter: counterReducer,
73+
dates: dateReducer,
74+
},
75+
});
76+
77+
// ======== COMPONENTS =========
78+
interface CounterProps {
79+
count: number;
80+
date: number | null;
81+
dispatch: Dispatch<AddAction | DateAction>;
5682
}
5783

58-
function App(): React.JSX.Element {
59-
const isDarkMode = useColorScheme() === 'dark';
84+
class CounterRaw extends React.PureComponent<CounterProps> {
85+
handleIncrement = () => {
86+
this.props.dispatch({type: ADD});
87+
};
6088

61-
const backgroundStyle = {
62-
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
89+
handleDate = () => {
90+
this.props.dispatch({type: DATE, payload: {date: Date.now()}});
6391
};
6492

65-
return (
66-
<SafeAreaView style={backgroundStyle}>
67-
<StatusBar
68-
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
69-
backgroundColor={backgroundStyle.backgroundColor}
70-
/>
71-
<ScrollView
72-
contentInsetAdjustmentBehavior="automatic"
73-
style={backgroundStyle}>
74-
<Header />
75-
<View
76-
style={{
77-
backgroundColor: isDarkMode ? Colors.black : Colors.white,
78-
}}>
79-
<Section title="Step One">
80-
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
81-
screen and then come back to see your edits.
82-
</Section>
83-
<Section title="See Your Changes">
84-
<ReloadInstructions />
85-
</Section>
86-
<Section title="Debug">
87-
<DebugInstructions />
88-
</Section>
89-
<Section title="Learn More">
90-
Read the docs to discover what to do next:
91-
</Section>
92-
<LearnMoreLinks />
93-
</View>
94-
</ScrollView>
95-
</SafeAreaView>
96-
);
93+
render() {
94+
return (
95+
<View style={{paddingVertical: 20}}>
96+
<Text>Counter Value: {this.props.count}</Text>
97+
<Text>date Value: {this.props.date}</Text>
98+
</View>
99+
);
100+
}
97101
}
98102

99-
const styles = StyleSheet.create({
100-
sectionContainer: {
101-
marginTop: 32,
102-
paddingHorizontal: 24,
103-
},
104-
sectionTitle: {
105-
fontSize: 24,
106-
fontWeight: '600',
107-
},
108-
sectionDescription: {
109-
marginTop: 8,
110-
fontSize: 18,
111-
fontWeight: '400',
112-
},
113-
highlight: {
114-
fontWeight: '700',
115-
},
103+
class ButtonsRaw extends React.PureComponent<CounterProps> {
104+
handleIncrement = () => {
105+
this.props.dispatch({type: ADD});
106+
};
107+
108+
handleDate = () => {
109+
this.props.dispatch({type: DATE, payload: {date: Date.now()}});
110+
};
111+
112+
render() {
113+
return (
114+
<View>
115+
<Button title="Update Date" onPress={this.handleDate} />
116+
<View style={{height: 20}} />
117+
<Button title="Increment Counter" onPress={this.handleIncrement} />
118+
</View>
119+
);
120+
}
121+
}
122+
123+
const mapStateToProps = (state: {counter: CounterState; dates: DateState}) => {
124+
return {count: state.counter.count, date: state.dates.date};
125+
};
126+
127+
const mapDispatchToProps = (dispatch: Dispatch<AddAction | DateAction>) => ({
128+
dispatch,
116129
});
117130

131+
const Buttons = connect(null, mapDispatchToProps)(ButtonsRaw);
132+
const Counter = connect(mapStateToProps, mapDispatchToProps)(CounterRaw);
133+
134+
class Container extends React.PureComponent {
135+
render() {
136+
return this.props.children;
137+
}
138+
}
139+
140+
const mapStateToPropsBreaking = (_state: any) => ({});
141+
142+
const ContainerBad = connect(mapStateToPropsBreaking, null)(Container);
143+
144+
const mapStateToPropsNonBlocking1 = (state: {counter: CounterState}) => ({
145+
count: state.counter.count,
146+
});
147+
148+
const ContainerNonBlocking1 = connect(
149+
mapStateToPropsNonBlocking1,
150+
null,
151+
)(Container);
152+
153+
const mapStateToPropsNonBlocking2 = (state: any) => ({state});
154+
155+
const ContainerNonBlocking2 = connect(
156+
mapStateToPropsNonBlocking2,
157+
null,
158+
)(Container);
159+
160+
class App extends React.Component {
161+
render() {
162+
const $H1: TextStyle = {fontSize: 20};
163+
return (
164+
<Provider store={store}>
165+
<Buttons />
166+
<Text style={$H1}>=Expected=</Text>
167+
<View>
168+
<Text>
169+
I don't have a parent blocking state updates so I should behave as
170+
expected
171+
</Text>
172+
<Counter />
173+
</View>
174+
175+
<Text style={$H1}>=Undesired behavior with react-redux 9.x=</Text>
176+
<ContainerBad>
177+
<Text>All redux state updates blocked</Text>
178+
<Counter />
179+
</ContainerBad>
180+
181+
<Text style={$H1}>=Partially working in 9.x=</Text>
182+
<ContainerNonBlocking1>
183+
<Text>
184+
I'm inconsistent, if date updates first I don't see it, but once
185+
count updates I rerender with count or date changes
186+
</Text>
187+
<Counter />
188+
</ContainerNonBlocking1>
189+
190+
<Text style={$H1}>=Poor workaround for 9.x?=</Text>
191+
<ContainerNonBlocking2>
192+
<Text>I see all state changes</Text>
193+
<Counter />
194+
</ContainerNonBlocking2>
195+
</Provider>
196+
);
197+
}
198+
}
199+
118200
export default App;

0 commit comments

Comments
 (0)