Skip to content

Commit 534ae26

Browse files
committed
feat: Move state below, use ref to force changes, clean up
1 parent cbcfce9 commit 534ae26

12 files changed

Lines changed: 748 additions & 106 deletions

README.md

Lines changed: 178 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,197 @@
1-
# react-native-multiswitch-controller
1+
# React Native Multi-Switch Controller
22

3-
Smooth animated multiswitch component with dynamic width
3+
A flexible and performant multi-switch controller for React Native with support for segmented controls and tabs.
4+
5+
## Features
6+
7+
- 🎯 **Zero Re-renders**: Access state outside provider without causing re-renders
8+
- 🎨 **Customizable**: Full control over colors, sizes, and styling
9+
-**Performant**: Optimized animations and state management
10+
- 🔄 **Flexible**: Support for segmented controls and tabs
11+
- 📱 **Accessible**: Built-in accessibility support
412

513
## Installation
614

7-
```sh
15+
```bash
816
npm install react-native-multiswitch-controller
917
```
1018

11-
## Usage
19+
## Basic Usage
20+
21+
```tsx
22+
import {
23+
ControlListProvider,
24+
PillSwitch,
25+
} from 'react-native-multiswitch-controller';
26+
27+
function MyComponent() {
28+
return (
29+
<ControlListProvider
30+
controlListProps={{
31+
options: [
32+
{ value: 'morning', label: '🌅' },
33+
{ value: 'afternoon', label: '☀️' },
34+
{ value: 'evening', label: '🌇' },
35+
{ value: 'night', label: '🌙' },
36+
],
37+
defaultOption: 'morning',
38+
variant: 'segmentedControl',
39+
}}
40+
>
41+
<PillSwitch
42+
inactiveBackgroundColor="rgba(59, 130, 246, 0.08)"
43+
activeBackgroundColor="rgb(37, 99, 235)"
44+
inactiveTextColor="rgb(37, 99, 235)"
45+
activeTextColor="rgb(253, 230, 138)"
46+
/>
47+
</ControlListProvider>
48+
);
49+
}
50+
```
51+
52+
## Accessing State Outside Provider
53+
54+
To avoid unnecessary re-renders while still accessing the control state, use one of these approaches:
55+
56+
### Option 1: Ref-based Access (Recommended)
1257

58+
```tsx
59+
import { useControlListStateRef } from 'react-native-multiswitch-controller';
1360

14-
```js
15-
import { multiply } from 'react-native-multiswitch-controller';
61+
function StateReader() {
62+
const stateRef = useControlListStateRef<string>();
1663

17-
// ...
64+
const handleGetCurrentValue = () => {
65+
const currentState = stateRef.current;
66+
if (currentState) {
67+
console.log('Current active option:', currentState.activeOption);
68+
console.log('All options:', currentState.options);
69+
}
70+
};
1871

19-
const result = await multiply(3, 7);
72+
return (
73+
<View>
74+
<Text>
75+
Current value: {stateRef.current?.activeOption || 'Loading...'}
76+
</Text>
77+
<Button title="Log State" onPress={handleGetCurrentValue} />
78+
</View>
79+
);
80+
}
2081
```
2182

83+
### Option 2: Event-based Subscription
2284

23-
## Contributing
85+
```tsx
86+
import { useControlListStateSubscription } from 'react-native-multiswitch-controller';
2487

25-
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
88+
function StateSubscriber() {
89+
const [lastChange, setLastChange] = useState<string>('None');
2690

27-
## License
91+
useControlListStateSubscription<string>((state) => {
92+
setLastChange(state.activeOption);
93+
console.log('State changed to:', state.activeOption);
94+
});
2895

29-
MIT
96+
return (
97+
<View>
98+
<Text>Last change: {lastChange}</Text>
99+
</View>
100+
);
101+
}
102+
```
103+
104+
## API Reference
105+
106+
### ControlListProvider
107+
108+
The provider component that manages the control list state.
109+
110+
```tsx
111+
<ControlListProvider
112+
controlListProps={{
113+
options: ControlOption<TValue>[];
114+
defaultOption: TValue;
115+
variant?: 'segmentedControl' | 'tabs';
116+
onPressCallback?: (value: TValue) => void;
117+
tabConfig?: { gap: number; padding: number };
118+
}}
119+
>
120+
{children}
121+
</ControlListProvider>
122+
```
123+
124+
### PillSwitch
125+
126+
The main switch component.
127+
128+
```tsx
129+
<PillSwitch
130+
align?: 'left' | 'right' | 'center';
131+
onPressCallback?: (value: TValue) => void;
132+
customItemStyle?: ViewStyle;
133+
containerHeight?: number;
134+
itemHeight?: number;
135+
inactiveBackgroundColor?: string;
136+
activeBackgroundColor?: string;
137+
inactiveTextColor?: string;
138+
activeTextColor?: string;
139+
customTextStyle?: TextStyle;
140+
/>
141+
```
30142

31-
---
143+
### Hooks
144+
145+
#### useControlListStateRef
146+
147+
Returns a ref to the current state without causing re-renders.
148+
149+
```tsx
150+
const stateRef = useControlListStateRef<TValue>();
151+
// Access state via stateRef.current
152+
```
153+
154+
#### useControlListStateSubscription
155+
156+
Subscribe to state changes without re-renders.
157+
158+
```tsx
159+
useControlListStateSubscription<TValue>((state) => {
160+
// Handle state changes
161+
});
162+
```
163+
164+
## Performance Benefits
165+
166+
- **Ref-based access**: No re-renders when reading state
167+
- **Event-based subscription**: Only re-renders when you explicitly handle changes
168+
- **Optimized animations**: Smooth transitions with minimal performance impact
169+
- **Memoized callbacks**: Prevents unnecessary re-renders in child components
170+
171+
## Types
172+
173+
```tsx
174+
type ControlOption<TValue> = {
175+
value: TValue;
176+
label: string;
177+
};
178+
179+
type ControlListState<TValue> = {
180+
options: ControlOption<TValue>[];
181+
activeOption: TValue;
182+
onChange: (value: TValue, callback?: () => void) => void;
183+
// ... other properties
184+
};
185+
```
186+
187+
## Contributing
188+
189+
1. Fork the repository
190+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
191+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
192+
4. Push to the branch (`git push origin feature/amazing-feature`)
193+
5. Open a Pull Request
194+
195+
## License
32196

33-
Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
197+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

example/src/Navigation.tsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,38 @@
11
import { createStaticNavigation } from '@react-navigation/native';
22
import { createNativeStackNavigator } from '@react-navigation/native-stack';
3-
import ExamplePillsScreen from './screens/ExamplePills';
3+
import { Button } from 'react-native';
4+
import ExampleInitialSetScreen from './screens/ExampleInitialSet';
5+
import TabsExampleScreen from './screens/TabsExample';
6+
import SegmentedControlExampleScreen from './screens/SegmentedControlExample';
47

5-
const RootStack = createNativeStackNavigator({
6-
initialRouteName: 'ExamplePills',
8+
export const RootStack = createNativeStackNavigator({
9+
initialRouteName: 'SegmentedControlExample',
710
screens: {
8-
ExamplePills: {
9-
screen: ExamplePillsScreen,
10-
options: {
11-
title: 'Example Pills',
12-
},
11+
SegmentedControlExample: {
12+
screen: SegmentedControlExampleScreen,
13+
options: ({ navigation }) => ({
14+
title: 'Segmeneted Control',
15+
headerRight: () => (
16+
<Button
17+
title="Tabs"
18+
onPress={() => {
19+
navigation.navigate('TabsExample');
20+
}}
21+
/>
22+
),
23+
}),
24+
},
25+
TabsExample: {
26+
screen: TabsExampleScreen,
27+
options: () => ({
28+
title: 'Example Tabs',
29+
}),
30+
},
31+
ExampleInitialSet: {
32+
screen: ExampleInitialSetScreen,
33+
options: () => ({
34+
title: 'Example Initial Based on Route',
35+
}),
1336
},
1437
},
1538
});

example/src/navigation.types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { StaticParamList } from '@react-navigation/native';
2+
import type { RootStack } from './Navigation';
3+
4+
type RootStackParamList = StaticParamList<typeof RootStack>;
5+
6+
declare global {
7+
namespace ReactNavigation {
8+
interface RootParamList extends RootStackParamList {}
9+
}
10+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { LinearGradient } from 'expo-linear-gradient';
2+
import { Button, StyleSheet, Text, View } from 'react-native';
3+
import { MultiswitchController } from 'react-native-multiswitch-controller';
4+
5+
import {
6+
useNavigation,
7+
type StaticScreenProps,
8+
} from '@react-navigation/native';
9+
import type { TimeOfDay } from '../types';
10+
11+
type ExampleInitialSetScreenProps = StaticScreenProps<{
12+
timeOfDay: TimeOfDay;
13+
}>;
14+
15+
export default function ExampleInitialSetScreen({
16+
route,
17+
}: ExampleInitialSetScreenProps) {
18+
const { timeOfDay } = route.params;
19+
20+
const navigation = useNavigation();
21+
22+
return (
23+
<LinearGradient colors={['#FFF5E6', '#FFE4B5']} style={styles.container}>
24+
<View style={styles.exampleContainer}>
25+
<Text style={styles.title}>Time of Day</Text>
26+
<MultiswitchController
27+
options={[
28+
{ value: 'morning', label: '🌅' },
29+
{ value: 'afternoon', label: '☀️' },
30+
{ value: 'evening', label: '🌇' },
31+
{ value: 'night', label: '🌙' },
32+
]}
33+
defaultOption="morning"
34+
variant="segmentedControl"
35+
onControlListStateChange={(value) => {
36+
console.log(
37+
'State ExampleInitialSetScreen onControlListStateChange changed:',
38+
value
39+
);
40+
}}
41+
segmentedControlProps={{
42+
inactiveBackgroundColor: 'rgba(59, 130, 246, 0.08)',
43+
activeBackgroundColor: 'rgb(37, 99, 235)',
44+
}}
45+
/>
46+
<Button
47+
title="Go to SegmentedControlExample"
48+
onPress={() => {
49+
navigation.navigate(
50+
'SegmentedControlExample',
51+
{
52+
timeOfDay: 'evening',
53+
},
54+
{ pop: true }
55+
);
56+
}}
57+
/>
58+
</View>
59+
<Text style={styles.title}>RERENDER TEST {timeOfDay}</Text>
60+
</LinearGradient>
61+
);
62+
}
63+
64+
const styles = StyleSheet.create({
65+
container: {
66+
flex: 1,
67+
gap: 16,
68+
padding: 10,
69+
},
70+
title: {
71+
fontSize: 18,
72+
padding: 10,
73+
},
74+
selectedText: {
75+
fontSize: 16,
76+
padding: 4,
77+
alignSelf: 'center',
78+
},
79+
bigContainer: {
80+
width: '100%',
81+
marginBottom: 20,
82+
},
83+
exampleContainer: {},
84+
smallText: {
85+
fontSize: 12,
86+
},
87+
stateReader: {
88+
marginTop: 20,
89+
padding: 10,
90+
backgroundColor: 'rgba(255, 255, 255, 0.8)',
91+
borderRadius: 8,
92+
},
93+
subtitle: {
94+
fontSize: 14,
95+
color: '#666',
96+
marginTop: 5,
97+
},
98+
button: {
99+
fontSize: 14,
100+
color: '#007AFF',
101+
marginTop: 10,
102+
textDecorationLine: 'underline',
103+
},
104+
});

0 commit comments

Comments
 (0)