How to add custom action in react-admin - react-admin

I use react-admin, and need to add a custom button to my list view that will navigate to a specific API.
My questions:
How to create the button? what shuold i write in my list?
How to navigate it to the API?
Thanks

My way of solving this:
create custom button:
import Button from '#material-ui/core/Button';
import { TopToolbar } from 'react-admin';
const PostShowActions = ({ basePath, data, resource }) => (
<TopToolbar>
{/* Add your custom actions */}
<Button color="primary" onClick={customAction}>Custom Action</Button>
</TopToolbar>
);
export const PostList = (props) => (
<List actions={<PostShowActions />} {...props}>
...
</List
);
navigate it to the API:
I implemented customAction like this:
const genarte = () => {
const httpClient = fetchUtils.fetchJson;
const apiUrl = "your API";
httpClient(`${apiUrl}`,{method: "POST"}).then(({ json }) => ({
data: json,
})
); };
I know it doesnt navigate the page to external link only make a http request,
but now for my need its ok.
if you have any comment or idea how to navigate the page to extranl link I would be happy

Related

Can an independent functional component re-render based on the state change of another?

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.

why button which is created in useEffect doesnt work correctly

i have two button and they call same function like this:
this button created in useEffect.
useEffect(() => {
navigation.setParams({
TopRightButton: (
<ConfirmButton
callback={() => ApiCall()}
/>
),
});
}, []);
this is second button in render
<TouchableOpacity onPress={()=> ApiCall()}><Text>Add</Text></TouchableOpacity>
this is function:
const ApiCall = () => {
console.log('name', Name);
};
i change name with useState. when i click TouchableOpacity which is second button, it shows me name. However, when i click first button which is created in useEffect, nothing happen. i mean, Name is null. why this is happen ?
any advice ?
EDITED:
const [Name, setName] = useState('');
Will u try this way?
const ApiCall = () => {
console.log('name', Name);
};
useEffect(() => {
navigation.setParams({
TopRightButton: (
<ConfirmButton
callback={() => ApiCall()}
/>
),
});
}, [Name]);
You should provide more information concerning that first button, im guessing it is a button that you want to show on the right side of your stack navigator's header, if that's the case you should NOT use setParams for that.
If you want to configure your navigator you should use navigationOptions.
So in the screen where you want to add a button in its header, you have to call setOptions inside useEffect instead. Check out the docs for more info on how to achieve that.

Prevent an element from rerendering on setState in React Native

I am using setState in order to dynamically update an image's source when a button is pushed in React Native. However, I am also using the TypeWriter library which types out text with a special 'typewriter' animated effect.
When my setState is called to change the element and the page is rerendered, TypeWriter types the text out again. I don't want this. Is there a way to exclude my TypeWriter text from being rerendered?
Code snippet:
export const AccountScreen = ({ navigation }) => {
this.state = {
img1Src: require('../assets/img/token.png')
}
const [state, setState] = useState(this.state);
changeImgSrc = () =>{
setState({
img1Src: require('../assets/img/X2.png')
})
}
return (
<TypeWriter> //I don't want this to re-render on setState
<Text>My Account</Text>
</TypeWriter>
<Animatable.Image source={state.img1Src}/>
<Button onPress={this.changeImgSrc}>
Click me!
</Button>
etc...//
export const NewComponent = ({ navigation }) => {
const [state, setState] = useState({
img1Src: require('../assets/img/token.png')
});
changeImgSrc = () =>{
setState({
img1Src: require('../assets/img/X2.png')
})
}
return (
<Animatable.Image source={state.img1Src}/>
<Button onPress={this.changeImgSrc}>
Click me!
</Button>
etc...//
You can make a new component is called NewComponent. Then,
export const AccountScreen = ({ navigation }) => {
return (
<TypeWriter> //I don't want this to re-render on setState
<Text>My Account</Text>
</TypeWriter>
<NewComponent />
etc...//
Also, you cannot use this in the functional component. In addition, if you want to call a function in functional component, you must use it with useCallback.

How do I shut of camera/scanner in react-native-qrcode-scanner?

Current code:
import QRCodeScanner from 'react-native-qrcode-scanner';
function ScanScreen({ navigation }) {
return (
<SafeAreaView style={styles.screen}>
<QRCodeScanner reactivate={true} reactivateTimeout={3000}
onRead={data => navigation.navigate('Third', {target:data.data})}
/>
</SafeAreaView>
);
}
It works, but here's what I want to do:
The user can navigate between screens, one of them being a QR code scanner.
While scanning, I need to debounce the scanner so it doesn't keep generating onRead events. The user could
a) start a scan in the scan screen without reading a QR code and navigate manually to another screen.
b) read a QR and automatically move to another screen for processing, then go back to scan again.
For this reason I need to re-enable the scanner after some reasonable time.
I can't just set reactivate to false because then the QR scanner is inactive until I restart the app.
The problem is that when the user stays in Another screen, the QR scanner re-activates after the timeout and tries to scan when it is not desired. What I ideally would like to do is to deactivate the QR scanner while the user is not in the scan screen, and re-activate it with the above mentioned parameters whenever the user enters the scan screen.
Is there any way to do this? Thanks!
I had almost the same problem. The scanner does not stop scanning while showing another View (using reactivate={true}). I am using react-navigation and so I came up to following solution.
You can listen on what is happening with your view with focus and blur.
this.props.navigation.addListener('focus', () => {
this.setState({viewFocused: true});
});
this.props.navigation.addListener('blur', () => {
this.setState({viewFocused: false});
});
Note: you place this piece of code in componentDidMount or using React.useEffect.
Based on the state viewFocused you can render the QR Code Scanner.
this.state.viewFocused && (
<QRCodeScanner
onRead={onRead}
reactivate={true}
reactivateTimeout={2000}
/> );
This helps me to solve my problem. To not scan while showing other views but to scan if the view is shown again. Credits to
pkyeck on github.com
as explained in the doc, you can do it progammatically
<QRCodeScanner
onRead={this.onSuccess}
ref={(node) => { this.scanner = node }} <-- add this
/>
and in your method (for example, in Alert or onPress button), reactivate the scanner like this:
onPress: () => {
this.scanner.reactivate()
},
With React hooks:
let scanner = useRef(null);
<QRCodeScanner
onRead={onSuccess}
ref={node => { scanner = node;}}
/>
Then you can attach it to a button. For example:
onPress={() => scanner.reactivate()}
Alternatively, Re-rendering screen with the useIsFocused hook.
React Navigation useIsFocused
import {useIsFocused} from '#react-navigation/native';
const ScannerCamera = ({navigation, route}) => {
const viewFocused = useIsFocused();
return (
viewFocused && (
<QRCodeScanner
containerStyle={{height: SCREEN_HEIGHT}}
cameraStyle={[{height: SCREEN_HEIGHT}]}
showMarker={true}
onRead={onSuccess}
flashMode={RNCamera.Constants.FlashMode.auto}
/>
)
);
};
Same thing as mentioned above in functional component or using hooks
import React, {useEffect, useState} from 'react';
const ScannerCamera = ({navigation, route}) => {
let uuId = null;
const [viewFocused, setViewFocused] = useState(false);
useEffect(() => {
const onFocus = navigation.addListener('focus', () => {
setViewFocused(true);
});
const onBlur = navigation.addListener('blur', () => {
setViewFocused(false);
});
return {onFocus, onBlur};
}, [navigation]);
return (
viewFocused && (
<QRCodeScanner
containerStyle={{height: SCREEN_HEIGHT}}
cameraStyle={[{height: SCREEN_HEIGHT}]}
showMarker={true}
onRead={onSuccess}
flashMode={RNCamera.Constants.FlashMode.auto}
/>
)
);
};

React Admin - Get current value in a form

I am having big troubles getting the "updated" value of a record in an edit form. I always get the initial record values, even though I have an input linked to the right record source, which should update it.
Is there an alternative way to get the values of the SimpleForm ?
I have a simple edit form :
<Edit {...props}>
<SimpleForm>
<MyEditForm {...props} />
</SimpleForm>
</Edit>
MyEditForm is as follow:
class MyEditForm extends React.Component {
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(prevProps.record.surface, this.props.record.surface); // <-- here is my problem, both values always get the initial value I had when I fetched the resource from API
}
render() {
return (
<div>
<TextInput source="surface" />
<!-- other fields -->
</div>
);
}
}
I usually do it this way to get my updated component's data from other components, but in the very case of a react-admin form, I can't get it to work.
Thanks,
Nicolas
It really depends on what you want to do with those values. If you want to hide/show/modify inputs based on the value of another input, the FormDataConsumer is the preferred method:
For example:
import { FormDataConsumer } from 'react-admin';
const OrderEdit = (props) => (
<Edit {...props}>
<SimpleForm>
<SelectInput source="country" choices={countries} />
<FormDataConsumer>
{({ formData, ...rest }) =>
<SelectInput
source="city"
choices={getCitiesFor(formData.country)}
{...rest}
/>
}
</FormDataConsumer>
</SimpleForm>
</Edit>
);
You can find more examples in the Input documentation. Take a look at the Linking Two Inputs and Hiding Inputs Based On Other Inputs.
However, if you want to use the form values in methods of your MyEditForm component, you should use the reduxForm selectors. This is safer as it will work even if we change the key where the reduxForm state is in our store.
import { connect } from 'react-redux';
import { getFormValues } from 'redux-form';
const mapStateToProps = state => ({
recordLiveValues: getFormValues('record-form')(state)
});
export default connect(mapStateToProps)(MyForm);
I found a working solution :
import { connect } from 'react-redux';
const mapStateToProps = state => ({
recordLiveValues: state.form['record-form'].values
});
export default connect(mapStateToProps)(MyForm);
When mapping the form state to my component's properties, I'm able to find my values using :
recordLiveValues.surface
If you don't want to use redux or you use other global state like me (recoil, etc.)
You can create custom-child component inside FormDataConsumer here example from me
// create FormReceiver component
const FormReceiver = ({ formData, getForm }) => {
useEffect(() => {
getForm(formData)
}, [formData])
return null
}
// inside any admin form
const AdminForm = () => {
const formState = useRef({}) // useRef for good performance not rerender
const getForm = (form) => {
formState.current = form
}
// you can access form by using `formState.current`
return (
<SimpleForm>
<FormDataConsumer>
{({ formData, ...rest }) => (
<FormReceiver formData={formData} getForm={getForm} />
)}
</FormDataConsumer>
</SimpleForm>
)
}