I'm implementing a solution in a React Native to avoid double taps on React Native Router Flux and I came up with this custom hook. What I really don't know is if this is a valid use case for this. Basically I created this as a hook because I'm using the useRef hook inside it. But maybe just a helper function would've been enough.
I also have a wrapper component for Pressable and a HOC for other components that may require onPress, this is for the current case where my press function call is neither of both.
Hook:
import { useRef } from 'react';
import { Platform } from 'react-native';
const timeout = Platform.OS === 'android' ? 2000 : 1000;
const useSafePress = () => {
const disabled = useRef(false);
const onSafePress = fn => {
if (!disabled.current) {
fn();
disabled.current = true;
setTimeout(() => {
disabled.current = false;
}, timeout);
}
};
return onSafePress;
};
export default useSafePress;
Usage:
const MyComponent = () => {
const onSafePress = useSafePress();
[...]
return (
<SomeOtherComponent
open={isOpen}
icon={isOpen ? 'window-close' : 'plus'}
actions={[
{
icon: 'share',
label: 'Action 1',
onPress: () => onSafePress(onPressShare),
},
{
icon: 'calendar-month',
label: 'Action 2',
onPress: () => onSafePress(onPressCalendar),
},
]}
color={white}
onStateChange={onStateChange}
/>
);
};
This varies on opinion for sure. In my opinion, you are just wrapping a singular function and maybe it doesn't have enough unique logic to warrant it being a hook.
Either way, this works, and it's really just a stylistic choice. In my projects, I try to only extract hooks when I find myself using a collection of hooks/logic over and over again in multiple places. Or there's lots of common logic that utilize a specific hook.
Related
I am using React native and I have a context variable post, it has an attribute called name and I have defined a function called onChange to set it.
import React, { useState } from "react";
const PostContext = React.createContext({
content: "",
onChange: (newPostContent: string) => {},
});
export const PostContextProvider = ({ children }) => {
const [content, setContent] = useState("");
const postChangeHandler = (newPostContent: string) => {
setContent(newPostContent);
};
return (
<PostContext.Provider
value={{ content, onChange: postChangeHandler }}
>
{children}
</PostContext.Provider>
);
};
export default PostContext;
Now I have a page on which I want to fetch a post from Amplify's GraphQL API and set its content to my context variable, so I can use it on other pages.
import React, { useEffect, useContext } from "react";
import { API, graphqlOperations} from "aws-amplify";
import PostContext from "./context/post-context";
const post = useContext(PostContext);
const fetchPost = async () => {
const {data: {getPost: { postContent },},} = await API.graphql(
graphqlOperation(`
query GetPost {
getPost(id: "${some post Id}") {
content
}
}
`)
);
post.onChange(postContent)
}
useEffect(()=>{
fetchPost()
}, [])
useEffect(()=>{
console.log(post.content)
}, [post])
What I expect is that in the async function, the execution is blocked until postContent (because of the await and then it's value is assigned to the context variable, or its update is schedualed (that's why I have also included a useEffect to console.log the value of post.content. But it is not updated and its value remains an empty screen. Can somebody help me with this? I am learning React native how this work, so a detailed answer that lets me know what I am doing wrong is appreciated.
I'm new to React Native, and my understanding is that functional components and hooks are the way to go. What I'm trying to do I've boiled down to the simplest case I can think of, to use as an example. (I am, by the way, writing in TypeScript.)
I have two Independent components. There is no parent-child relationship between the two. Take a look:
The two components are a login button on the navigation bar and a switch in the enclosed screen. How can I make the login button be enabled when the switch is ON and disabled when the switch is OFF?
The login button looks like this:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const handleClick = () => {
navigation.navigate('Away');
};
// I want the 'disabled' value to update based on the state of the switch.
return (
<Button title="Login"
color="white"
disabled={false}
onPress={handleClick} />
);
};
As you can see, right now I've simply hard-coded the disabled setting for the button. I'm thinking that will no doubt change to something dynamic.
The screen containing the switch looks like this:
const HomeScreen = () => {
const [isEnabled, setEnabled] = useState(false);
const toggleSwitch = () => setEnabled(value => !value);
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
</SafeAreaView>
);
};
What's throwing me for a loop is that the HomeScreen and LoginButton are setup like this in the navigator stack. I can think of no way to have the one "know" about the other:
<MainStack.Screen name="Home"
component={HomeScreen}
options={{title: "Home", headerRight: LoginButton}} />
I need to get the login button component to re-render when the state of the switch changes, but I cannot seem to trigger that. I've tried to apply several different things, all involving hooks of some kind. I have to confess, I think I'm missing at least the big picture and probably some finer details too.
I'm open to any suggestion, but really I'm wondering what the simplest, best-practice (or thereabouts) solution is. Can this be done purely with functional components? Do I have to introduce a class somewhere? Is there a "notification" of sorts (I come from native iOS development). I'd appreciate some help. Thank you.
I figured out another way of tracking state, for this simple example, that doesn't involve using a reducer, which I'm including here for documentation purposes in hopes that it may help someone. It tracks very close to the accepted answer.
First, we create both a custom hook for the context, and a context provider:
// FILE: switch-context.tsx
import React, { SetStateAction } from 'react';
type SwitchStateTuple = [boolean, React.Dispatch<SetStateAction<boolean>>];
const SwitchContext = React.createContext<SwitchStateTuple>(null!);
const useSwitchContext = (): SwitchStateTuple => {
const context = React.useContext(SwitchContext);
if (!context) {
throw new Error(`useSwitch must be used within a SwitchProvider.`);
}
return context;
};
const SwitchContextProvider = (props: object) => {
const [isOn, setOn] = React.useState(false);
const [value, setValue] = React.useMemo(() => [isOn, setOn], [isOn]);
return (<SwitchContext.Provider value={[value, setValue]} {...props} />);
};
export { SwitchContextProvider, useSwitchContext };
Then, in the main file, after importing the SwitchContextProvider and useSwitchContext hook, wrap the app's content in the context provider:
const App = () => {
return (
<SwitchContextProvider>
<NavigationContainer>
{MainStackScreen()}
</NavigationContainer>
</SwitchContextProvider>
);
};
Use the custom hook in the Home screen:
const HomeScreen = () => {
const [isOn, setOn] = useSwitchContext();
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={setOn}
value={isOn}
/>
</SafeAreaView>
);
};
And in the Login button component:
const LoginButton = (): JSX.Element => {
const navigation = useNavigation();
const [isOn] = useSwitchContext();
const handleClick = () => {
navigation.navigate('Away');
};
return (
<Button title="Login"
color="white"
disabled={!isOn}
onPress={handleClick} />
);
};
I created the above by adapting an example I found here:
https://kentcdodds.com/blog/application-state-management-with-react
The whole project is now up on GitHub, as a reference:
https://github.com/software-mariodiana/hellonavigate
If you want to choose the context method, you need to create a component first that creates our context:
import React, { createContext, useReducer, Dispatch } from 'react';
type ActionType = {type: 'TOGGLE_STATE'};
// Your initial switch state
const initialState = false;
// We are creating a reducer to handle our actions
const SwitchStateReducer = (state = initialState, action: ActionType) => {
switch(action.type){
// In this case we only have one action to toggle state, but you can add more
case 'TOGGLE_STATE':
return !state;
// Return the current state if the action type is not correct
default:
return state;
}
}
// We are creating a context using React's Context API
// This should be exported because we are going to import this context in order to access the state
export const SwitchStateContext = createContext<[boolean, Dispatch<ActionType>]>(null as any);
// And now we are creating a Provider component to pass our reducer to the context
const SwitchStateProvider: React.FC = ({children}) => {
// We are initializing our reducer with useReducer hook
const reducer = useReducer(SwitchStateReducer, initialState);
return (
<SwitchStateContext.Provider value={reducer}>
{children}
</SwitchStateContext.Provider>
)
}
export default SwitchStateProvider;
Then you need to wrap your header, your home screen and all other components/pages in this component. Basically you need to wrap your whole app content with this component.
<SwitchStateProvider>
<AppContent />
</SwitchStateProvider>
Then you need to use this context in your home screen component:
const HomeScreen = () => {
// useContext returns an array with two elements if used with useReducer.
// These elements are: first element is your current state, second element is a function to dispatch actions
const [switchState, dispatchSwitch] = useContext(SwitchStateContext);
const toggleSwitch = () => {
// Here, TOGGLE_STATE is the action name we have set in our reducer
dispatchSwitch({type: 'TOGGLE_STATE'})
}
return (
<SafeAreaView>
<Switch
style={styles.switch}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={switchState}
/>
</SafeAreaView>
);
};
And finally you need to use this context in your button component:
// We are going to use only the state, so i'm not including the dispatch action here.
const [switchState] = useContext(SwitchStateContext);
<Button title="Login"
color="white"
disabled={!switchState}
onPress={handleClick} />
Crete a reducer.js :
import {CLEAR_VALUE_ACTION, SET_VALUE_ACTION} from '../action'
const initialAppState = {
value: '',
};
export const reducer = (state = initialAppState, action) => {
if (action.type === SET_VALUE_ACTION) {
state.value = action.data
}else if(action.type===CLEAR_VALUE_ACTION){
state.value = ''
}
return {...state};
};
Then action.js:
export const SET_VALUE_ACTION = 'SET_VALUE_ACTION';
export const CLEAR_VALUE_ACTION = 'CLEAR_VALUE_ACTION';
export function setValueAction(data) {
return {type: SET_VALUE_ACTION, data};
}
export function clearValueAction() {
return {type: CLEAR_VALUE_ACTION}
}
In your components :
...
import {connect} from 'react-redux';
...
function ComponentA({cartItems, dispatch}) {
}
const mapStateToProps = (state) => {
return {
value: state.someState,
};
};
export default connect(mapStateToProps)(ComponentA);
You can create more components and communicate between them, independently.
How can I assert that a button is disabled in React Native Testing Library? I would imagine something like:
expect(getByRole('button')).toBeDisabled()
but RNTL doesn't provide toBeDisabled assertion.
this is a common issue due to RN nature. I have managed to reach my goal by just testing the actual effect of callback function, not just comparing the number of calls or something like that...
describe('<Button /> - ', () => {
let state = false
const onPressMock = jest.fn(() => {
state = !state
})
const props: ButtonProps = {
text: 'Submit',
onPress: onPressMock
}
it('should become disabled', () => {
// act: render container
const { container } = render(<Button {...props} isDisabled />)
// assert 1: check if button receives {isDisabled}
expect(container.props.isDisabled).toEqual(true)
// act2: fire callback
fireEvent(container, 'onPress')
// assert 2: "state" should remain as false.
expect(state).toEqual(false)
})
})
make sure that your button looks like:
const isBlockedInteraction: boolean = isLoading || isDisabled;
return (
<TouchableOpacity
onPress={!isBlockedInteraction && onPress}
disabled={isBlockedInteraction}
{...props}
/>
)
Quite a simple try toHaveProperty method, I hope that helped.
example:
import React from 'react'
import {fireEvent, render} from '#testing-library/react-native';
import {SignInScreen} from './SignInScreen';
it('disabled button if email and password are empty', () => {
const screen = render(<SignInScreen />);
const button = screen.getByText('Login');
// screen.debug();
// console.log(button.props);
expect(button.props).toHaveProperty('disabled', true);
});
I'm using NativeBase in a React Native app. I'm trying to show a Toast component based on an error that is set in an redux action because it happens via a call to the API.
It will show now, but currently I get the warning message:
Warning: Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
I'm not sure how I could bind this or what I could do to solve the warning.
Render Method
render() {
return (
<View>
{this.renderError()}
{this.renderForm()}
</View>
);
}
Render Error Method
renderError() {
if (this.props.error.type === 'server') {
return (
Toast.show({
text: this.props.error.message,
buttonText: 'Okay',
duration: 5000,
type: 'danger'
})
);
}
}
Versions
React Native: 0.55.4
Native Base: 2.4.5
Edit: Adding an example for clarity
I need to show a Toast based on the response from the server. For example, if the username and password don't match an account, I need to render the Toast.
Solution:
I ended up creating a ToastService:
import { Toast } from 'native-base';
function showToast(message) {
return (
Toast.show({
text: message,
buttonText: 'Okay',
duration: 5000,
type: 'danger'
})
);
}
export default {
showToast
};
and now in my action I can just call:
ToastService.showToast(data);
You can create a function and call this one outside. But make sure your app is wrap with the Root component of native-base. No need to return a component like you do. Calling this function gonna show the toastr and now you have the freedom to call from anywhere. But make sure Root component wrap your app.
import { Toast } from 'native-base';
export const toastr = {
showToast: (message, duration = 2500) => {
Toast.show({
text: message,
duration,
position: 'bottom',
textStyle: { textAlign: 'center' },
buttonText: 'Okay',
});
},
};
Now inside your action you can call the toastr function
toastr.showToast('Verication code send to your phone.');
Or in redux actions
const signup = values => dispatch => {
try {
// your logic here
} catch (error) {
toastr.showToast(error.message)
}
}
I solved this issue by using React Hooks.
() => {
useEffect(() => {
if(error) {
Toast.show({
text: this.props.error.message,
buttonText: 'Okay',
duration: 5000,
type: 'danger'
})
}
})
return (
<View>
{this.renderForm()}
</View>
);
}
Check React Native Seed for this implementation
https://reactnativeseed.com/
Like Dwayne says above, you need to use useEffect so that Toast is called before the render cycle. You can wrap this is a component like so:
const ErrorToast: React.FC = () => {
const {state} = useCollections();
useEffect(() => {
if(state.errored) {
Toast.show({
text: 'Oops. There has been an error',
duration: 2000
});
}
});
return null;
}
And then simply include it as <ErrorToast />
I used arrow function inside of my React component to avoid binding this context, for example my component look like this;
class Comp extends Component {
_fn1 = () => {}
_fn2 = () => {}
render() {
return (<div></div>);
}
}
How do I test _fn1 and _fn2 function in my test cases? Because these kind of function did not associated with React component itself, so when I do
fnStub = sandbox.stub(Comp.prototype, "_fn1");
it is not going work, since _fn did not bind with Comp.prototype. Thus, how can I test those functions in React if I want to create function with arrow syntax? Thanks!
ES6 functions or arrow functions are not added to the class prototype.
However, there are a couple of ways to test them:-
Test that the functions themselves are called when a suitable event occurs
ES5 functions exist on the class prototype and something like this is possible:
import Component from 'path/to/component';
import { shallow } from 'enzyme';
describe(<Component>, () => {
it('should call handleSubmit', () => {
const spy = jest.spyOn(Component.prototype, 'handleSubmit');
const wrapper = shallow(<Component />);
...
//Invoke handleSubmit
...
expect(spy).toBeCalled()
});
});
whereas ES6 functions exist on the instance of the mounted component(you can also use shallow)
import Component from 'path/to/component';
import { mount } from 'enzyme';
describe(<Component>, () => {
it('should call handleSubmit', () => {
const wrapper = mount(<Component />);
...
const spy = jest.spyOn(wrapper.instance(), 'handleSubmit');
//update the instance with the new spy
wrapper.instance().forceUpdate();
...
//invoke handleSubmit
expect(spy).toBeCalled()
});
});
Test their functionality by simulating actions that will invoke these functions and test for the expected behavior
Assuming component content such as:
state = {
title: 'Current Title'
};
updateTitle = (event) => {
title = event.target.value;
this.setState({ title });
}
render() {
return (
<div>
<input type="text" value={this.state.title} onChange={this.updateTitle} />
<div>
)
}
Test
...
wrapper.find('input').simulate('change', {target: {value: 'New title'}});
expect(wrapper.state().title).toBe('New Title');
...
I hope this helps.
In general I find it easier to test that these functions have resulted in a correct component state, rather than test the function itself. For example, here is a component that toggles a state variable when a button is clicked:
class MyComponent extends Component {
state = {
toggle: false
}
_fn1 = () => {
this.setState(previousState => ({
toggle: !previousState.toggle
});
}
render() {
const { toggle } = this.state;
return (
<button onClick={this.clickHandler}>
Turn me {toggle ? 'on' : 'off'}
</button>
);
}
}
My preferred approach here would be to test the component as a whole, i.e. the "unit" of the unit test is the component. The test would therefore find the button, simulate a click, and ensure that the correct text is shown. This may not be a textbook unit test, but it achieves the goal of testing the component.
Using sinon/chai/mocha/enzyme:
describe('My Component', () => {
it('alternates text display when the button is clicked', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper).to.have.text('Turn me off');
wrapper.find('button').simulate('click');
expect(wrapper).to.have.text('Turn me on');
});
});