React native redux saga TypeError - javascript

First of all, I apologise for the long post, I tried to shortened as much as possible.
I am trying to dispatch an action through saga, but I am getting TypeError: resultFunc.apply is not a function error.
The function sets a two variables in the Application reducer:
isFirstTimer: false
appLanguage: en
Store/App/Actions.js
const { Types, Creators } = createActions({
// Set language for the app from user
firstTimer: ['language'],
setDeviceLanguage: ['language'],
})
export const AppTypes = Types
export default Creators
Store/App/Reducers.js
import { INITIAL_STATE } from './InitialState'
import { createReducer } from 'reduxsauce'
import { AppTypes } from './Actions'
export const firstTimer = (state, { language }) => ({
...state,
isFristTimer: false,
appLanguage: language,
})
export const reducer = createReducer(INITIAL_STATE, {
[AppTypes.FIRST_TIMER]: firstTimer,
[AppTypes.SET_DEVICE_LANGUAGE]: setDeviceLanguage,
})
Store/App/Selector.js
import { createSelector } from 'reselect'
const isFristTimer = (state) => state.isFristTimer
const language = (state) => state.language
export const getFirstTimer = createSelector([isFristTimer, language])
Sagas/AppSaga.js
import { put, select } from 'redux-saga/effects'
import * as Selectors from 'App/Stores/App/Selectors'
export function* firstTimer() {
const whereTo = yield select(Selectors.getFirstTimer)
const language = yield select(Selectors.language)
Reactotron.log('getFirstTimer value', whereTo)
Reactotron.log('language value', language)
// When those operations are finished we redirect to the main screen
}
Note: The two variables from the selectors do not log in Reactotron!!.
Sagas/index.js
import { takeLatest, all } from 'redux-saga/effects'
import { firstTimer } from './AppSaga'
import { AppTypes } from 'App/Stores/App/Actions'
export default function* root() {
yield all([
// Call `firstTimer()` when a `FIRST_TIMER` action is triggered
takeLatest(AppTypes.FIRST_TIMER, firstTimer),
])
In my rootScreen.js, I am rendering conditionally according to the mentioned above firstTime variable
componentDidMount() {
Reactotron.log('our state : ' + this.props.app)
}
_setLanguage(language) {
this.props.setFirstTimer(language)
}
render() {
const { isFristTimer } = this.props.app
if (isFristTimer) {
return (
<View style={Helpers.fill}>
<ImageBackground source={Images.firstTimerBg} style={Helpers.fullSize}>
<View
style={[
Style.buttonWrapper,
Helpers.mainCenter,
Helpers.mainSpaceBetween,
Helpers.row,
Metrics.mediumHorizontalMargin,
]}
>
<TouchableOpacity
onPress={() => this._setLanguage('en')}
style={[Style.buttonContainer, Helpers.center]}
>
<Text style={Fonts.normal}>{Words.english}</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => this._setLanguage('ar')}
style={[Style.buttonContainer, Helpers.center]}
>
<Text style={Fonts.normal}>{Words.arabic}</Text>
</TouchableOpacity>
</View>
</ImageBackground>
</View>
)
}
return (
<View style={Helpers.fill}>
<AppNavigator
// Initialize the NavigationService (see https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html)
ref={(navigatorRef) => {
NavigationService.setTopLevelNavigator(navigatorRef)
}}
/>
</View>
)
}
}
The end result :
If I dismiss the error, the application is no longer rendering the first block, and it renders the AppNavigator

The issue is with the getFirstTimer selector - the second argument to createSelector should be the resultFunc (that you see referenced in your error) that takes the results of other selectors it depends on and returns a value. See https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc. Updating getFirstTimer should resolve the issue:
import { createSelector } from 'reselect'
const isFristTimer = (state) => state.isFristTimer
const language = (state) => state.language
export const getFirstTimer = createSelector(
[isFristTimer, language],
// here is where the result func is added to the selector definition
(isFristTimer, language) => ({
isFristTimer,
language,
})
)

Related

React Native - Type Script: How to save dark mode toggle state even after turning off the application?

What is the correct way so that I can save the dark mode switch even after turning off the application?
I want to use the use-state-persist library to achieve the goal.
In my example I show app.tsx , Preferences.tsx, ViewField.tsx .
So that it will be possible to understand how the logic is built
DarkModeContext
import React from 'react';
interface DarkMode {
isDarkMode: boolean;
setDarkMode: () => void;
}
export const DarkModeContext = React.createContext<DarkMode>({} as DarkMode);
this is the app.tsx
import React, { useEffect, useState } from 'react';
import { syncStorage } from 'use-state-persist';
const App: () => ReactElement = () => {
const [isDarkMode, setDarkMode] = useState(false);
const Drawer = createDrawerNavigator();
const initStorage = async () => await syncStorage.init();
const toggleDarkMode = () => {
setDarkMode(!isDarkMode);
};
return (
<DarkModeContext.Provider value={{ isDarkMode, toggleDarkMode }}>
<NavigationContainer>
<Drawer.Navigator
drawerContent={SideMenu}
screenOptions={{
drawerPosition: 'right',
headerShown: false,
drawerType: 'front',
}}
>
<Drawer.Screen name='HomeScreen' component={StackNavigator} />
</Drawer.Navigator>
</NavigationContainer>
</DarkModeContext.Provider>
);
};
export default App;
this is the Preferences.tsx
import React, { useContext, useState } from 'react';
import ViewField from './ViewField';
import { DarkModeContext } from '~/context/DarkModeContext';
const Preferences = () => {
const { isDarkMode, toggleDarkMode } = useContext(DarkModeContext);
return (
<View>
<ViewField title='dark mode' isEnabled={isDarkMode} setValue={toggleDarkMode} />
</View>
);
};
export default Preferences;
this is the ViewField.tsx
import { View, Text, Switch } from 'react-native';
import React from 'react';
import styles from './ViewFieldStyles';
import { useContext } from 'react';
import { DarkModeContext } from '~/context/DarkModeContext';
type Props = {
title: string;
isEnabled: boolean;
setValue: () => void;
};
const ViewField = ({ title, isEnabled, setValue }: Props) => {
const { isDarkMode } = useContext(DarkModeContext);
return (
<View style={isDarkMode ? styles.optionViewDark : styles.optionView}>
<View style={styles.sameRowTextView}>
<Text style={isDarkMode ? styles.optionTextDark : styles.optionText}>{title}</Text>
<View style={styles.switchView}>
<Switch
trackColor={
isDarkMode
? { false: 'rgba(255, 255, 255, 0.38)', true: 'rgba(187, 134, 252, 0.38)' }
: { false: '#767577', true: 'rgba(4, 76, 163, 0.38)' }
}
onValueChange={setValue}
value={isEnabled}
/>
</View>
</View>
</View>
);
};
export default ViewField;
Keep in mind that there seems to be some problems in use-state-persist using Boolean values. Furthermore, the latest published version of this library is from 2020.
However, the use-state-persist library just seems to be a wrapper around AsyncStorage, which is very well maintained. I would encourage you to use this library instead.
In your case, this could be implemented as follows:
Store the actual setter of the state in the context,
Create an effect that accesses the async storage on mount of the application: if there exists a value for the corresponding key, set the state of the context, if not, then do nothing.
In the Preferences component, store a new state in the async storage as well.
const App: () => ReactElement = () => {
const [isDarkMode, setDarkMode] = useState(false);
const contextValue = React.useMemo(() => ({
isDarkMode,
setDarkMode
}), [isDarkMode])
React.useEffect(() => {
const load = async () => {
const value = await AsyncStorage.getItem('isDarkMode');
if (value !== null) {
setDarkMode(JSON.parse(value));
}
}
load();
}, [])
return (
<DarkModeContext.Provider value={contextValue}>
...
};
In the Preferences component, set the state and save it to the local storage.
const Preferences = () => {
const { isDarkMode, setDarkMode } = useContext(DarkModeContext);
async function toggle() {
const newValue = JSON.stringify(!isDarkMode);
await AsyncStorage.setItem('isDarkMode', newValue);
setDarkMode(prev => !prev);
}
return (
<View>
<ViewField title='dark mode' isEnabled={isDarkMode} setValue={toggle} />
</View>
);
}

React test a component with saga

Hllo Guys, I'm having a bit trouble with testing my component
The problem is that I would like to test my React Native Component that uses saga to fetch data from server.
The Problem is that I do know what I'm supposed to do, I think I should mock my API calls in my test file but I do not know how :/
The component file is really simple, when mounted it dispatches action to fetch list on vehicles, and then it shows them in UI. And until that is fetched it shows loading text
Bellow are my current setup of components & test file.
Here is a screen component that fetches initial data on screen load
Screen Component
import React, { useContext, useEffect, useState } from 'react';
import { Platform, FlatList, View, ActivityIndicator, Text } from 'react-native';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { vehiclesActions } from '_store/vehicles';
export const MainScreen = ({ navigation }) => {
/**
* Redux selectors and dispatch
*/
const {
loading = true,
vehicles = [],
loadMore = false
} = useSelector((state) => state.vehicles);
/**
* Initial effect, fetches all vehicles
*/
useEffect(() => {
dispatch(
vehiclesActions.vehicleGet({
page: 1,
})
);
}, []);
const renderCard = () => {
return (<View><Text>Test</Text></View>)
}
if (loading) {
return (<View><Text>App Loading </Text></View>
}
return (
<View style={styles.wrapper}>
<View
style={
Platform.OS === 'ios' ? { marginTop: 30 } : { marginTop: 0, flex: 1 }
}
>
{!loading && (
<View style={Platform.OS === 'ios' ? {} : { flex: 1 }}>
<FlatList
testID={'flat-list'}
data={vehicles}
renderItem={renderCard}
/>
</View>
)}
</View>
</View>
);
};
MainScreen.propTypes = {
navigation: PropTypes.object
};
export default MainScreen;
My Vehicles Saga:
const api = {
vehicles: {
getVehicles: (page) => {
return api.get(`/vehicles/list?page=${page}`, {});
},
}
function* getVehicles(action) {
try {
const { page } = action.payload;
const { data } = yield call(api.vehicles.getVehicles, page);
yield put({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload: data });
} catch (err) {
yield call(errorHandler, err);
yield put({ type: vehiclesConstants.VEHICLE_GET_FAIL });
}
}
export function* vehiclesSaga() {
yield takeLatest(vehiclesConstants.VEHICLE_GET_REQUEST, getVehicles);
}
Actions:
export const vehiclesActions = {
vehicleGet: payload => ({ type: vehiclesConstants.VEHICLE_GET_REQUEST, payload }),
vehicleGetSuccess: payload => ({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload }),
vehicleGetFail: error => ({ type: vehiclesConstants.VEHICLE_GET_FAIL, error }),
}
Reducer
import { vehiclesConstants } from "./constants";
const initialState = {
vehicles: [],
loading: true,
};
export const vehiclesReducer = (state = initialState, action) => {
switch (action.type) {
case vehiclesConstants.VEHICLE_GET_REQUEST:
return {
...state,
loading: true,
};
case vehiclesConstants.VEHICLE_GET_SUCCESS:
return {
...state,
loading: false,
vehicles: action.payload,
};
}
}
My Test File
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
Like I see above I cannot find element with testId flat-list, since component AppScreen it always show loading text, is there any way I could mock that API call and make this to work ?
Jest allows you to mock any module using jest.mock.
You have to write an alternative to axios.get like this
const vehiclesData = [
// ... put default data here
]
const delay = (ms, value) =>
new Promise(res => setTimeout(() => res(value), ms))
const mockAxiosGet = async (path) => {
let result = null
if (path.includes('vehicles/list') {
const query = new URLSearchParams(path.replace(/^[^?]+\?/, ''))
const page = + query.get('page')
const pageSize = 10
const offset = (page - 1)*pageSize
result = vehiclesData.slice(offset, offset + pageSize)
}
return delay(
// simulate 100-500ms latency
Math.floor(100 + Math.random()*400),
{ data: result }
)
}
Then modify the test file as
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import axios from 'axios'
// enable jest mock on 'axios' module
jest.mock('axios')
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
before(() => {
// mock axios implementation
axios.get.mockImplementation(mockAxiosGet)
})
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
For your use case, read more at Mocking Implementations

Cannot read property 'map' of undefined - React Custom Hooks / useContext one works / one doesn't

I am currently following a tutorial on youtube building a whatsapp clone with react and socket.io from webDevSimplified: https://www.youtube.com/watch?v=tBr-PybP_9c
and here the main repo : 'https://github.com/WebDevSimplified/Whatsapp-Clone/tree/master/client
I got stuck halfway through as for some reason my custom useConversation hook returns undefined while my useContacts hook works without any problems.
Here the setup:
App.js :
import Dashboard from "./Dashboard";
import { ContactsProvider } from "../contexts/ContactsProvider";
import { ConversationsProvider } from "../contexts/ConversationsProvider";
import Test from "../components/test";///test
console.log(Test)//test purpose
function App() {
const [id, setId] = useLocalStorage("id");
const dashboard = (
<ContactsProvider>
<ConversationsProvider id={id}>
<Dashboard id={id} />
</ConversationsProvider>
</ContactsProvider>
);
return id ? dashboard : <Login onIdSubmit={setId} />;
}
export default App;
ContactsProvider.js
import React, { useContext } from "react";
import useLocalStorage from "../hooks/useLocalStorage";
const ContactsContext = React.createContext();
export function useContacts() {
return useContext(ContactsContext);
}
export function ContactsProvider({ children }) {
const [contacts, setContacts] = useLocalStorage("contacts", []);//initalValue an empty array
function createContact(id, name) {
setContacts((prevContacts) => {
return [...prevContacts, { id, name }];
});
}
return (
<ContactsContext.Provider value={{ contacts, createContact }}>
{children}
</ContactsContext.Provider>
);
}
Contacts.js - here my useContacts works
import React from "react";
import { ListGroup } from "react-bootstrap";
import { useContacts } from "../contexts/ContactsProvider";
export default function Contacts() {
const { contacts } = useContacts();
console.log(`contacts: ${contacts}`); //returns object, object as expected
return (
<ListGroup variant="flush">
{contacts.map((contact) => (//visibly working in UI when commenting out the ListGroup in conversations.js
<ListGroup.Item key={contact.id}>{contact.name}</ListGroup.Item>
))}
</ListGroup>
);
}
Here the problematic part:
ConversationsProvider.js
import React, { useContext } from "react";
import useLocalStorage from "../hooks/useLocalStorage";
import { useContacts } from "./ContactsProvider";
const ConversationsContext = React.createContext();
export function useConversations() {
return useContext(ConversationsContext);
}
export function ConversationsProvider({ children }) {
const [conversations, setConversations] = useLocalStorage(
"conversations", []);//as well empty array
const { contacts } = useContacts();
function createConversation(recipients) {
setConversations((prevConversations) => {
return [...prevConversations, { recipients, messages: [] }];
});
}
const formattedConversations = conversations.map((conversation) => {
const recipients = conversation.recipients.map((recipient) => {
const contact = contacts.find((contact) => {
return contact.id === recipient;
});
const name = (contact && contact.name) || recipient;
return { id: recipient, name };
});
return { ...conversation, recipients };
});
const value = {
conversations: formattedConversations,
createConversation,
};
return (
<ConversationsContext.Provider value={{ value }}>
{children}
</ConversationsContext.Provider>
);
}
and the component that causes the error:
Conversations.js:
import React from "react";
import { ListGroup } from "react-bootstrap";
import { useConversations } from "../contexts/ConversationsProvider";
export default function Conversations() {
const { conversations } = useConversations();
console.log( `conversations: ${conversations}`)//returns undefined
return (
<ListGroup variant="flush">
{conversations.map((conversation, index) => (//can't map because conversations is undefined
<ListGroup.Item key={index}>
{conversation.recipients
.map(r => r.name)
.join(", ")}
</ListGroup.Item>
))}
</ListGroup>
);
}
Here the localStorage setup for clarity:
import { useEffect, useState } from 'react'
const PREFIX = 'whatsapp-clone-'
export default function useLocalStorage(key, initialValue) {
const prefixedKey = PREFIX + key;
const [value, setValue] = useState(() => {
const jsonValue = localStorage.getItem(prefixedKey);
if (jsonValue != null) return JSON.parse(jsonValue);
if (typeof initialValue === "function") {
return initialValue();
} else {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(prefixedKey, JSON.stringify(value));
}, [prefixedKey, value]);
return [value, setValue];
}
I have been trying to solve this problem for hours and running out of ideas. I setup a test.js and imported the hooks in a Test.js function. Both return their objects respectively.
I have created the application using npx create-react-app and running it via yarn.
The problem lies within your ConversationsProvider.js:
const value = {
conversations: formattedConversations,
createConversation,
};
return (
<ConversationsContext.Provider value={{ value }}>
{children}
</ConversationsContext.Provider>
);
The following lines are all the same:
<ConversationsContext.Provider value={{ value }}>
<ConversationsContext.Provider value={{ value: value }}>
<ConversationsContext.Provider value={{ value: { conversations: formattedConversations, createConversation: createConversation } }}>
When you execute:
const { conversations } = useConversations();
conversations will be set to undefined because the object returned will only have the property value.
To fix the issue remove the nesting by changing the line:
<ConversationsContext.Provider value={{ value }}>
// to
<ConversationsContext.Provider value={value}>
PS. Here a tip to improve your debugging skills. You already did:
const { conversations } = useConversations();
console.log(conversations);
Which logged undefined. The next step would be to not destruct the object immediately and log the whole context value instead.
const contextValue = useConversations();
console.log(contextValue);
This would have shown that the object returned by useConversations is not what you expect it to be.

How to persist data in react native app with usememo hook

I'm writing an application in react native and I came across a problem - the application will have several screens (I use react-navigation and react-navigation-tabs) and two-color themes (light and dark) managed by context and hooks. What I would like to achieve is the selected theme to be remembered by the app (the light theme will be set as default, and after switching to dark, leaving the application and returning the dark theme should still be applied).
EDIT #2: One answer from yesterday (that disappeared for some reason) suggested the use of redux and local storage so I'm editing the paragraph below to clarify the situation.
Easiest way would be to use sync storage/localStorage (I already have working version of the app using local storage), but one tutorial I found on the web uses the user memo hook for this purpose, and while it should work, it isn't (in my case at least), and I don't know why...
My App.js file below:
imports ...
const TabNavigator = createBottomTabNavigator({
Home: Home,
List: List,
});
const App = createAppContainer(TabNavigator);
export default () => (
<ThemeProvider>
<App />
</ThemeProvider>
ThemeContext.js file:
imports ...
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [colors, setColors] = useState(themes.lightTheme) //setting light theme as default
const value = useMemo(
() => ({
colors,
setColors,
}),
[colors, setColors],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
And Home.js file, with a button to switch between themes:
imports ...
export const Home = () => {
const { colors, setColors } = useContext(ThemeContext);
const toggleTheme = () => {
if (colors.type === 'light') {
setColors(themes.darkTheme);
} else {
setColors(themes.lightTheme);
}
}
return (
<>
<View style={{...styles.mainView, backgroundColor: colors.backgroundColor }}>
<Text style={{...styles.mainText, color: colors.color}}>Hello Native World</Text>
<Button title='toggle theme' onPress={toggleTheme} />
</View>
</>
)
}
const styles = StyleSheet.create({
mainView: {
paddingTop: 40,
display: 'flex',
alignItems: 'center',
},
mainText: {
fontSize: 40,
fontWeight: 'bold',
},
});
The key file you have to change is your context file:
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [colors, setColors] = useState(themes.lightTheme) //setting light theme as default
const value = useMemo(
() => ({
colors,
setColors,
}),
[colors, setColors],
);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
I don't really understand why you use useMemo, but I will leave it. From a quick glance I'd say that it is not needed, but I don't know your app. What you want is something like this:
import AsyncStorage from '#react-community/async-storage'
export const ThemeContext = createContext()
export function usePersistedState(key, initialState) {
const [state, setState] = useState(() => {})
useEffect(() => {
async function getAndSetInitialState() {
const persistedState = await AsyncStorage.getItem(key)
if (persistedState) {
setState(JSON.parse(persistedState))
} else if (typeof initialState === 'function') {
return setState(initialState())
} else {
return setState(initialState)
}
}
getAndSetInitialState()
}, [key])
function setPersistedState(value) {
AsyncStorage.setItem(key, JSON.stringify(value))
setState(value)
}
return [state, setPersistedState]
}
export const ThemeProvider = ({ children }) => {
const [colors, setColors] = usePersistedState("your_storage_key", themes.lightTheme) //setting light theme as default
const value = useMemo(
() => ({
colors,
setColors,
}),
[colors, setColors]
)
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
I might have missed some edge cases, but this way your app will load it's state from the storage and save it's state into the storage.
EDIT: I'm not sure how useMemo would help, AsyncStorage is the easiest solution imo.

Prevent Double tap in React native

How to prevent a user from tapping a button twice in React native?
i.e. A user must not be able tap twice quickly on a touchable highlight
https://snack.expo.io/#patwoz/withpreventdoubleclick
Use this HOC to extend the touchable components like TouchableHighlight, Button ...
import debounce from 'lodash.debounce'; // 4.0.8
const withPreventDoubleClick = (WrappedComponent) => {
class PreventDoubleClick extends React.PureComponent {
debouncedOnPress = () => {
this.props.onPress && this.props.onPress();
}
onPress = debounce(this.debouncedOnPress, 300, { leading: true, trailing: false });
render() {
return <WrappedComponent {...this.props} onPress={this.onPress} />;
}
}
PreventDoubleClick.displayName = `withPreventDoubleClick(${WrappedComponent.displayName ||WrappedComponent.name})`
return PreventDoubleClick;
}
Usage
import { Button } from 'react-native';
import withPreventDoubleClick from './withPreventDoubleClick';
const ButtonEx = withPreventDoubleClick(Button);
<ButtonEx onPress={this.onButtonClick} title="Click here" />
Use property Button.disabled
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, View, Button } from 'react-native';
export default class App extends Component {
state={
disabled:false,
}
pressButton() {
this.setState({
disabled: true,
});
// enable after 5 second
setTimeout(()=>{
this.setState({
disabled: false,
});
}, 5000)
}
render() {
return (
<Button
onPress={() => this.pressButton()}
title="Learn More"
color="#841584"
disabled={this.state.disabled}
accessibilityLabel="Learn more about this purple button"
/>
);
}
}
// skip this line if using Create React Native App
AppRegistry.registerComponent('AwesomeProject', () => App);
Here is my simple hook.
import { useRef } from 'react';
const BOUNCE_RATE = 2000;
export const useDebounce = () => {
const busy = useRef(false);
const debounce = async (callback: Function) => {
setTimeout(() => {
busy.current = false;
}, BOUNCE_RATE);
if (!busy.current) {
busy.current = true;
callback();
}
};
return { debounce };
};
This can be used anywhere you like. Even if it's not for buttons.
const { debounce } = useDebounce();
<Button onPress={() => debounce(onPressReload)}>
Tap Me again and adain!
</Button>
Agree with Accepted answer but very simple way , we can use following way
import debounce from 'lodash/debounce';
componentDidMount() {
this.onPressMethod= debounce(this.onPressMethod.bind(this), 500);
}
onPressMethod=()=> {
//what you actually want on button press
}
render() {
return (
<Button
onPress={() => this.onPressMethod()}
title="Your Button Name"
/>
);
}
I use it by refer the answer above. 'disabled' doesn't have to be a state.
import React, { Component } from 'react';
import { TouchableHighlight } from 'react-native';
class PreventDoubleTap extends Component {
disabled = false;
onPress = (...args) => {
if(this.disabled) return;
this.disabled = true;
setTimeout(()=>{
this.disabled = false;
}, 500);
this.props.onPress && this.props.onPress(...args);
}
}
export class ButtonHighLight extends PreventDoubleTap {
render() {
return (
<TouchableHighlight
{...this.props}
onPress={this.onPress}
underlayColor="#f7f7f7"
/>
);
}
}
It can be other touchable component like TouchableOpacity.
If you are using react navigation then use this format to navigate to another page.
this.props.navigation.navigate({key:"any",routeName:"YourRoute",params:{param1:value,param2:value}})
The StackNavigator would prevent routes having same keys to be pushed in the stack again.
You could write anything unique as the key and the params prop is optional if you want to pass parameters to another screen.
The accepted solution works great, but it makes it mandatory to wrap your whole component and to import lodash to achieve the desired behavior.
I wrote a custom React hook that makes it possible to only wrap your callback:
useTimeBlockedCallback.js
import { useRef } from 'react'
export default (callback, timeBlocked = 1000) => {
const isBlockedRef = useRef(false)
const unblockTimeout = useRef(false)
return (...callbackArgs) => {
if (!isBlockedRef.current) {
callback(...callbackArgs)
}
clearTimeout(unblockTimeout.current)
unblockTimeout.current = setTimeout(() => isBlockedRef.current = false, timeBlocked)
isBlockedRef.current = true
}
}
Usage:
yourComponent.js
import React from 'react'
import { View, Text } from 'react-native'
import useTimeBlockedCallback from '../hooks/useTimeBlockedCallback'
export default () => {
const callbackWithNoArgs = useTimeBlockedCallback(() => {
console.log('Do stuff here, like opening a new scene for instance.')
})
const callbackWithArgs = useTimeBlockedCallback((text) => {
console.log(text + ' will be logged once every 1000ms tops')
})
return (
<View>
<Text onPress={callbackWithNoArgs}>Touch me without double tap</Text>
<Text onPress={() => callbackWithArgs('Hello world')}>Log hello world</Text>
</View>
)
}
The callback is blocked for 1000ms after being called by default, but you can change that with the hook's second parameter.
I have a very simple solution using runAfterInteractions:
_GoCategoria(_categoria,_tipo){
if (loading === false){
loading = true;
this.props.navigation.navigate("Categoria", {categoria: _categoria, tipo: _tipo});
}
InteractionManager.runAfterInteractions(() => {
loading = false;
});
};
Did not use disable feature, setTimeout, or installed extra stuff.
This way code is executed without delays. I did not avoid double taps but I assured code to run just once.
I used the returned object from TouchableOpacity described in the docs https://reactnative.dev/docs/pressevent and a state variable to manage timestamps. lastTime is a state variable initialized at 0.
const [lastTime, setLastTime] = useState(0);
...
<TouchableOpacity onPress={async (obj) =>{
try{
console.log('Last time: ', obj.nativeEvent.timestamp);
if ((obj.nativeEvent.timestamp-lastTime)>1500){
console.log('First time: ',obj.nativeEvent.timestamp);
setLastTime(obj.nativeEvent.timestamp);
//your code
SplashScreen.show();
await dispatch(getDetails(item.device));
await dispatch(getTravels(item.device));
navigation.navigate("Tab");
//end of code
}
else{
return;
}
}catch(e){
console.log(e);
}
}}>
I am using an async function to handle dispatches that are actually fetching data, in the end I'm basically navigating to other screen.
Im printing out first and last time between touches. I choose there to exist at least 1500 ms of difference between them, and avoid any parasite double tap.
You can also show a loading gif whilst you await some async operation. Just make sure to tag your onPress with async () => {} so it can be await'd.
import React from 'react';
import {View, Button, ActivityIndicator} from 'react-native';
class Btn extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false
}
}
async setIsLoading(isLoading) {
const p = new Promise((resolve) => {
this.setState({isLoading}, resolve);
});
return p;
}
render() {
const {onPress, ...p} = this.props;
if (this.state.isLoading) {
return <View style={{marginTop: 2, marginBottom: 2}}>
<ActivityIndicator
size="large"
/>
</View>;
}
return <Button
{...p}
onPress={async () => {
await this.setIsLoading(true);
await onPress();
await this.setIsLoading(false);
}}
/>
}
}
export default Btn;
My implementation of wrapper component.
import React, { useState, useEffect } from 'react';
import { TouchableHighlight } from 'react-native';
export default ButtonOneTap = ({ onPress, disabled, children, ...props }) => {
const [isDisabled, toggleDisable] = useState(disabled);
const [timerId, setTimerId] = useState(null);
useEffect(() => {
toggleDisable(disabled);
},[disabled]);
useEffect(() => {
return () => {
toggleDisable(disabled);
clearTimeout(timerId);
}
})
const handleOnPress = () => {
toggleDisable(true);
onPress();
setTimerId(setTimeout(() => {
toggleDisable(false)
}, 1000))
}
return (
<TouchableHighlight onPress={handleOnPress} {...props} disabled={isDisabled} >
{children}
</TouchableHighlight>
)
}

Categories

Resources