-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: makeMutable documentation and examples (#6261)
## Summary This PR adds `makeMutable` function documentation with the usage example. --------- Co-authored-by: Tomek Zawadzki <tomasz.zawadzki@swmansion.com> Co-authored-by: Krzysztof Piaskowy <krzysztof.piaskowy@swmansion.com>
- Loading branch information
1 parent
0616843
commit da2e2ef
Showing
3 changed files
with
332 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
--- | ||
sidebar_position: 9 | ||
--- | ||
|
||
# makeMutable | ||
|
||
:::caution | ||
The usage of `makeMutable` is discouraged in most cases. It's recommended to use the [`useSharedValue`](/docs/core/useSharedValue) hook instead unless you know what you're doing and you are aware of the consequences (see the [Remarks](#remarks) section). | ||
|
||
`makeMutable` is used internally and its behavior may change over time. | ||
::: | ||
|
||
`makeMutable` is a function internally used by the [`useSharedValue`](/docs/core/useSharedValue) hook to create a [shared value](/docs/fundamentals/glossary#shared-value). | ||
|
||
It makes it possible to create mutable values without the use of the hook, which can be useful in some cases (e.g. in the global scope, as an array of mutable values, etc.). | ||
|
||
The created object is, in fact, the same as the one returned by `useSharedValue` hook, so the further usage is the same. | ||
|
||
## Reference | ||
|
||
```javascript | ||
import { makeMutable } from 'react-native-reanimated'; | ||
|
||
const mv = makeMutable(100); | ||
``` | ||
|
||
<details> | ||
<summary>Type definitions</summary> | ||
|
||
```typescript | ||
interface SharedValue<Value = unknown> { | ||
value: Value; | ||
addListener: (listenerID: number, listener: (value: Value) => void) => void; | ||
removeListener: (listenerID: number) => void; | ||
modify: ( | ||
modifier?: <T extends Value>(value: T) => T, | ||
forceUpdate?: boolean | ||
) => void; | ||
} | ||
|
||
function makeMutable<Value>(initial: Value): SharedValue<Value>; | ||
``` | ||
|
||
</details> | ||
|
||
### Arguments | ||
|
||
#### `initial` | ||
|
||
The value you want to be initially stored to a `.value` property. It can be any JavaScript value like `number`, `string` or `boolean` but also data structures such as `array` and `object`. | ||
|
||
### Returns | ||
|
||
`makeMutable` returns a mutable value with a single `value` property initially set to the `initial`. | ||
|
||
Data stored in mutable values can be accessed and modified by their `.value` property. | ||
|
||
## Example | ||
|
||
import MakeMutable from '@site/src/examples/MakeMutable'; | ||
import MakeMutableSrc from '!!raw-loader!@site/src/examples/MakeMutable'; | ||
|
||
<InteractiveExample src={MakeMutableSrc} component={MakeMutable} showCode /> | ||
|
||
## Remarks | ||
|
||
:::info | ||
We use _mutable value_ name for an object created by `makeMutable` to distinguish it from the _shared value_ created by `useSharedValue`. Technically, _shared value_ is a _mutable value_ with an automatic cleanup. | ||
::: | ||
|
||
- All remarks from the [useSharedValue](/docs/core/useSharedValue) hook apply to `makeMutable` as well. | ||
|
||
- Don't call `makeMutable` directly in the component scope. When component re-renders, it will create the completely new object (with the new `initial` value if it was changed) and the state of the previous mutable value will be lost. | ||
|
||
<Indent> | ||
|
||
```javascript | ||
function App() { | ||
const [counter, setCounter] = useState(0); | ||
const mv = makeMutable(counter); // π¨ creates a new mutable value on each render | ||
|
||
useEffect(() => { | ||
const interval = setInterval(() => { | ||
setCounter((prev) => prev + 1); // updates the counter stored in the component state | ||
}, 1000); | ||
|
||
return () => { | ||
clearInterval(interval); | ||
}; | ||
}, [mv]); | ||
|
||
useAnimatedReaction( | ||
() => mv.value, | ||
(value) => { | ||
console.log(value); // prints 0, 1, 2, ... | ||
} | ||
); | ||
} | ||
``` | ||
|
||
</Indent> | ||
|
||
- Use `cancelAnimation` to stop all animations running on the mutable value if it's no longer needed and there are still some animations running. Be super careful with infinite animations, as they will never stop unless you cancel them manually. | ||
|
||
<Indent> | ||
|
||
```javascript | ||
function App() { | ||
const mv = useMemo(() => makeMutable(0), []); | ||
|
||
useEffect(() => { | ||
mv.value = withRepeat(withSpring(100), -1, true); // creates an infinite animation | ||
|
||
return () => { | ||
cancelAnimation(mv); // β stops the infinite animation on component unmount | ||
}; | ||
}, []); | ||
} | ||
``` | ||
|
||
</Indent> | ||
|
||
- You don't have to use `cancelAnimation` when the value is not animated. It will be garbage collected automatically when no more references to it exist. | ||
|
||
<Indent> | ||
|
||
```javascript | ||
const someFlag = makeMutable(false); | ||
|
||
function App() { | ||
someFlag.value = true; // β no need to cancel the animation later on | ||
} | ||
``` | ||
|
||
</Indent> | ||
|
||
- When you decide to use `makeMutable`, ensure that you follow [Rules of React](https://react.dev/reference/rules) and avoid common `useRef` pitfalls, such as modifying the reference during rendering (see the **Pitfall** section in the [useRef](https://react.dev/reference/react/useRef) documentation for more details). | ||
|
||
### Comparison with `useSharedValue` | ||
|
||
| `makeMutable` | `useSharedValue` | | ||
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | ||
| Creates a new object on each call | Reuses the same object on each call | | ||
| If `initial` value changes, a new object with the new value is created | If `initialValue` value changes, the initially created object is returned without any changes | | ||
| Can be used outside of the component scope | Can be used only inside the component scope | | ||
| Can be used in loops (also when the number of iterations is not constant) | Can be used in loops only if the number of rendered hooks (`useSharedValue` calls) is constant | | ||
| Doesn't automatically cancel animations when the component is unmounted | Automatically cancels animations when the component is unmounted | | ||
|
||
## Platform compatibility | ||
|
||
<div className="platform-compatibility"> | ||
|
||
| Android | iOS | Web | | ||
| ------- | --- | --- | | ||
| β | β | β | | ||
|
||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import { | ||
Text, | ||
StyleSheet, | ||
View, | ||
TouchableOpacity, | ||
useColorScheme, | ||
} from 'react-native'; | ||
|
||
import React, { useCallback, useMemo, useState } from 'react'; | ||
import Animated, { | ||
makeMutable, | ||
runOnJS, | ||
runOnUI, | ||
useAnimatedStyle, | ||
} from 'react-native-reanimated'; | ||
import type { SharedValue } from 'react-native-reanimated'; | ||
|
||
type CheckListSelectorProps = { | ||
items: string[]; | ||
onSubmit: (selectedItems: string[]) => void; | ||
}; | ||
|
||
function CheckListSelector({ items, onSubmit }: CheckListSelectorProps) { | ||
const checkListItemProps = useMemo( | ||
() => | ||
items.map((item) => ({ | ||
item, | ||
// highlight-next-line | ||
selected: makeMutable(false), | ||
})), | ||
[items] | ||
); | ||
|
||
const handleSubmit = useCallback(() => { | ||
runOnUI(() => { | ||
const selectedItems = checkListItemProps | ||
.filter((props) => props.selected.value) | ||
.map((props) => props.item); | ||
|
||
runOnJS(onSubmit)(selectedItems); | ||
})(); | ||
}, [checkListItemProps, onSubmit]); | ||
|
||
return ( | ||
<View style={styles.checkList}> | ||
{checkListItemProps.map((props) => ( | ||
<CheckListItem key={props.item} {...props} /> | ||
))} | ||
<TouchableOpacity style={styles.submitButton} onPress={handleSubmit}> | ||
<Text style={styles.submitButtonText}>Submit</Text> | ||
</TouchableOpacity> | ||
</View> | ||
); | ||
} | ||
|
||
type CheckListItemProps = { | ||
item: string; | ||
selected: SharedValue<boolean>; | ||
}; | ||
|
||
function CheckListItem({ item, selected }: CheckListItemProps) { | ||
const scheme = useColorScheme(); | ||
const onPress = useCallback(() => { | ||
// highlight-start | ||
// No need to update the array of selected items, just toggle | ||
// the selected value thanks to separate shared values | ||
runOnUI(() => { | ||
selected.value = !selected.value; | ||
})(); | ||
// highlight-end | ||
}, [selected]); | ||
|
||
return ( | ||
<TouchableOpacity style={styles.listItem} onPress={onPress}> | ||
{/* highlight-start */} | ||
{/* No need to use `useDerivedValue` hook to get the `selected` value */} | ||
<CheckBox value={selected} /> | ||
{/* highlight-end */} | ||
<Text | ||
style={[ | ||
styles.listItemText, | ||
{ color: scheme === 'dark' ? 'white' : 'black' }, | ||
]}> | ||
{item} | ||
</Text> | ||
</TouchableOpacity> | ||
); | ||
} | ||
|
||
type CheckBoxProps = { | ||
value: SharedValue<boolean>; | ||
}; | ||
|
||
function CheckBox({ value }: CheckBoxProps) { | ||
const checkboxTickStyle = useAnimatedStyle(() => ({ | ||
opacity: value.value ? 1 : 0, | ||
})); | ||
|
||
return ( | ||
<View style={styles.checkBox}> | ||
<Animated.View style={[styles.checkBoxTick, checkboxTickStyle]} /> | ||
</View> | ||
); | ||
} | ||
|
||
const ITEMS = [ | ||
'π Cat', | ||
'π Dog', | ||
'π¦ Duck', | ||
'π Rabbit', | ||
'π Mouse', | ||
'π Rooster', | ||
]; | ||
|
||
export default function App() { | ||
const [selectedItems, setSelectedItems] = useState<string[]>([]); | ||
const scheme = useColorScheme(); | ||
|
||
return ( | ||
<View style={styles.container}> | ||
<CheckListSelector items={ITEMS} onSubmit={setSelectedItems} /> | ||
<Text style={{ color: scheme === 'dark' ? 'white' : 'black' }}> | ||
Selected items:{' '} | ||
{selectedItems.length ? selectedItems.join(', ') : 'None'} | ||
</Text> | ||
</View> | ||
); | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
container: { | ||
flex: 1, | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
}, | ||
checkList: { | ||
gap: 8, | ||
padding: 16, | ||
}, | ||
listItem: { | ||
flexDirection: 'row', | ||
alignItems: 'center', | ||
gap: 12, | ||
}, | ||
listItemText: { | ||
fontSize: 20, | ||
}, | ||
checkBox: { | ||
width: 16, | ||
height: 16, | ||
borderRadius: 4, | ||
borderWidth: 1, | ||
padding: 2, | ||
borderColor: '#b58df1', | ||
}, | ||
checkBoxTick: { | ||
flex: 1, | ||
borderRadius: 2, | ||
backgroundColor: '#b58df1', | ||
}, | ||
submitButton: { | ||
backgroundColor: '#b58df1', | ||
color: 'white', | ||
alignItems: 'center', | ||
borderRadius: 4, | ||
padding: 8, | ||
marginTop: 16, | ||
}, | ||
submitButtonText: { | ||
color: 'white', | ||
fontSize: 16, | ||
fontWeight: 'bold', | ||
}, | ||
}); |