React Admin: how to pass state to transform - react-admin

I have a component for creating media which uploads the media first to S3, then puts the returned values into the component's state:
import { Create, ReferenceInput, SelectInput, SimpleForm, TextInput } from 'react-admin';
import { Field } from 'react-final-form';
import React, { useState } from 'react';
import { ImageHandler } from './ImageHandler';
import { BeatLoader } from 'react-spinners';
export const MediaCreate = props => {
const [image, setImage] = useState(null);
const [isUploading, setIsUploading] = useState(false);
console.log(image) // <-- this contains the image object after uploading
const transform = data => {
console.log(image) // <-- this is NULL after clicking submit
return {
...data,
key: image.key,
mime_type: image.mime
}
};
return (
<Create {...props} transform={transform}>
<SimpleForm>
<SelectInput source="collection" label="Type" choices={[
{ id: 'gallery', name: 'Gallery' },
{ id: 'attachment', name: 'Attachment' },
]}/>
<ReferenceInput label="Asset" source="asset_id" reference="assets">
<SelectInput optionText="name"/>
</ReferenceInput>
<TextInput source={'name'} />
{isUploading &&
<div style={{ display: 'flex', alignItems: 'center' }}>
<BeatLoader
size={10}
color={"#123abc"}
loading={isUploading}
/> Uploading, please wait
</div>
}
<ImageHandler
isUploading={(isUploading) => setIsUploading(isUploading)}
onUploaded={(image) => setImage(image)}
/>
</SimpleForm>
</Create>
);
};
Why is image null despite containing the value after upload? How can I pass in my component state to the transform function?

This can be reached by useRef
export const MediaCreate = props => {
const [image, setImage] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const someRef = useRef()
someRef.current = image
const transform = data => {
console.log(someRef.current) // <-- this will not be NULL
return {
...data,
key: someRef.current.key,
mime_type: someRef.current.mime
}
};

Related

How can I access the values in formik before submitting in react native?

Here, is the RegisterScreen, that I want to change the formik values before submitting.
function RegisterScreen({ navigation }) {
const [hidePassword, setHidePassword] = useState(true);
const [showDatePicker, setShowDatePicker] = useState(false);
return (
<ScrollView>
<View style={{ flex: 1 }}>
<AppImageBackground>
<AppForm
initialValues={{ name: '', date: '', checkBox: '', email: '', password: '', phone: '', gender: null, city: null, bloodGroup: null }}
onSubmit={(values) => {
navigation.navigate("Login Successfully")
console.log(values);
}}
validationSchema={validationschema}
>
//FormFields
//FormFields
<SubmitButton title={"Register"} />
</AppForm>
</AppImageBackground>
</View>
</ScrollView>
);
}
How can I access the values and change it before submitting. Here below is Formik component.
Note I want to change the values in RegisterScreen (above code).
function AppForm({ initialValues, onSubmit, validationSchema, children }) {
return (
<View style={styles.container}>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{() => (<>{children}</>)}
</Formik>
</View>
);
}
You can use the onSubmit handler to access / change the values -
<Formik
onSubmit={(values, actions) => {
const phone = values.phone.replace(...)
const valuesToSend = { ...values, phone }
onSubmit(valuesToSend, actions) //from props
}
}
You can use useFormik as below
Make common validation and scheme file
useCreateUserForm.tsx
import * as Yup from 'yup';
import {FormikHelpers, useFormik} from 'formik';
export interface IUserFromType {
name: string;
}
const defaultValues: IUserFromType = {
name: '',
};
const schema = Yup.object().shape({
name: Yup.string().min(4, 'Group Name must be at least 4 characters').required("Group Name is required field"),
});
export const useCreateUserForm = (
onSubmit: (
values: IUserFromType,
formikHelpers: FormikHelpers<IUserFromType>,
) => void | Promise<unknown>,
initialValues: IUserFromType = defaultValues,
) => {
return useFormik<IUserFromType>({
initialValues,
enableReinitialize: true,
validationSchema: schema,
validateOnChange: false,
validateOnBlur: true,
onSubmit,
});
};
After that you need to add main render file like below
const formik = useCreateUserForm(onSubmit);
<UserForm formik={formik} />
Now Create UserForm.tsx
import React, {useState} from 'react';
import {FormikProps, FormikProvider} from 'formik';
interface IGroupFormProps {
formik: FormikProps<IUserFromType>;
}
function UserForm(props: IGroupFormProps) {
const {formik} = props;
const {values, handleChange, handleBlur, setFieldValue, setFieldError} = formik;
return (
<FormikProvider value={formik}>
<View>
<TextInput
onChangeText={handleChange('name')}
onBlur={handleBlur('name')}
value={values.name}
/>
</View>
</FormikProvider>
);
}
export default UserForm;
Same render all your component inside FormikProvider. Hope This will resolve your issue.

Testing react-native app with jest. Problem in accessing context and Provider

I know the title is very vague but I hope someone may have an idea.
I want to perform a simple snapshot test on one of my screens with jest but I keep getting errors like this:
Warning: React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
14 | test('renders correctly', async () => {
15 | const tree = renderer.create(
> 16 | <AuthContext.AuthProvider>
| ^
17 | <AuthContext.Consumer>
18 | <ValidateScreenPhrase ref={(navigator)=>{ setNavigator(navigator) }}/>
19 | </AuthContext.Consumer>
The problem is probably that I use a Context build that looks as follows:
import React, { useReducer } from 'react'
export default (reducer, actions, defaultValue) => {
const Context = React.createContext();
const Provider = ({children}) => {
const [state, dispatch] = useReducer(reducer, defaultValue)
const boundActions = {}
for (let key in actions){
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>{children}</Context.Provider>
)
}
return { Context, Provider }
}
from here I then build different Contexts that contain functions and states as e.g.:
import createDataContext from "./createDataContext"
import { navigate } from '../navigationRef'
const authReducer = (state, action) => {
switch (action.type){
case 'clear_error_message':
return { ...state, errorMessage: '' }
default:
return state
}
}
const validateInput = (dispatch) => {
return (userInput, expected) => {
if (userInput === expected) {
navigate('done')
}
else{dispatch({ type: 'error_message', payload: 'your seed phrase was not typed correctly'})}
}
}
export const { Provider, Context } = createDataContext(
authReducer,
{ clearErrorMessage },
{ errorMessage: '' }
)
Now the screen that I want to test is this:
import React, { useState, useContext, useEffect } from 'react'
import { StyleSheet, View, TextInput, SafeAreaView } from 'react-native'
import { Text, Button } from 'react-native-elements'
import { NavigationEvents } from 'react-navigation'
import { Context as AuthContext } from '../context/AuthContext'
import BackButton from '../components/BackButton'
const ValidateSeedPhraseScreen = ({navigation}) => {
const { validateInput, clearErrorMessage, state } = useContext(AuthContext)
const [seedPhrase, setSeedPhrase] = useState('')
const testPhrase = 'blouse'
const checkSeedPhrase = () => {
validateInput(seedPhrase, testPhrase)}
return (
<SafeAreaView style={styles.container}>
<NavigationEvents
onWillFocus={clearErrorMessage}
/>
<NavigationEvents />
<BackButton routeName='walletInformation'/>
<View style={styles.seedPhraseContainer}>
<Text h3>Validate Your Seed Phrase</Text>
<TextInput
style={styles.input}
editable
multiline
onChangeText={(text) => setSeedPhrase(text)}
value={seedPhrase}
placeholder="Your Validation Seed Phrase"
autoCorrect={false}
autoCapitalize='none'
maxLength={200}
/>
<Button
title="Validate"
onPress={() => checkSeedPhrase(seedPhrase, testPhrase)}
style={styles.validateButton}
/>
{state.errorMessage ? (<Text style={styles.errorMessage}>{state.errorMessage}</Text> ) : null}
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginLeft: 25,
marginRight: 25
},
seedPhraseContainer:{
marginTop: '40%'
},
input: {
height: 200,
margin: 12,
borderWidth: 1,
padding: 10,
fontSize: 20,
borderRadius: 10
},
validateButton:{
paddingBottom: 15
}
})
export default ValidateSeedPhraseScreen
Here I import the AuthContext and make use of the function validateInput and state from the Context. Here I also don't know how to bring these into the testing file
and my test so far looks like this:
import React, {useContext} from "react";
import renderer from 'react-test-renderer';
import { setNavigator } from '../../src/navigationRef';
import ValidateScreenPhrase from '../../src/screens/ValidateSeedPhraseScreen'
import { Provider as AuthProvider, Context as AuthContext } from '../../src/context/AuthContext';
jest.mock('react-navigation', () => ({
withNavigation: ValidateScreenPhrase => props => (
<ValidateScreenPhrase navigation={{ navigate: jest.fn() }} {...props} />
), NavigationEvents: 'mockNavigationEvents'
}));
test('renders correctly', async () => {
const tree = renderer.create(
<AuthProvider>
<AuthContext.Consumer>
<ValidateScreenPhrase ref={(navigator)=>{ setNavigator(navigator) }}/>
</AuthContext.Consumer>
</AuthProvider>, {}).toJSON();
expect(tree).toMatchSnapshot();
});
I already tried out all lot of changes with the context and provider structure. I then always get errors like: "Authcontext is undefined" or "render is not a function".
Does anyone have an idea about how to approach this?

Cannot access global store redux when implementing isolated store for specific screen

I got this error when using useSelector to access variable from global store of Redux.
But Redux store for specific screen is still available.
Here is my code for specific screen:
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
productDetailReducer,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(productDetailWatcher);
interface IProductDetailProps {}
const Product = () => {
const product = useSelector((state: IProductDetailState) => state.product);
const products = useSelector((state: IStoreState) => state.productsState.products);
const dispatch: Dispatch = useDispatch();
const handlePress = () => {
const product = {
id: 1,
thumbImage: 'https://aladin-today-bucket.s3.ap-southeast-1.amazonaws.com/sm/4bd144c7-896c-55db-b70d-7b5a0b2d4638.jpeg',
thumbHeight: 192,
productName: 'Váy xường xám mặc Trung Thu,Tết'
};
dispatch(GetProductDetailRequest(product));
}
console.log(`Product Detail 2 render with ${product}`);
console.log(`Products ${products}`);
return (
<View style={styles.container}>
<Button onPress={handlePress} title='Get Product Detail' />
{product && (
<View style={styles.productContainer}>
<Image style={styles.image} source={{ uri: product.thumbImage }} />
<Text style={styles.name}>{product.productName}</Text>
</View>
)}
</View>
);
};
const ProductDetail: React.FC<IProductDetailProps> = () => {
console.log("Product Detail rendering...");
return <Provider store={store}>
<Product />
</Provider>;
};
reducer for ProductDetail:
import { IProductDetailState, ProductDetailActions, ProductDetailActionType } from './types';
import { ProductModel } from 'models/Product';
import AsyncStorage from '#react-native-community/async-storage';
import { PersistConfig, persistReducer } from 'redux-persist';
const productDetailState: IProductDetailState = {
product: undefined,
loading: false,
}
export const productDetailReducer = (state = productDetailState, action: ProductDetailActions): IProductDetailState => {
switch(action.type) {
case ProductDetailActionType.GET_PRODUCT_DETAIL_REQUEST: {
console.log('Enter');
return {
...state,
loading: true
}
}
case ProductDetailActionType.GET_PRODUCT_DETAIL_SUCCESS: {
const productDetail: ProductModel = action.payload;
return {
...state,
product: productDetail,
loading: false
}
}
case ProductDetailActionType.GET_PRODUCT_DETAIL_FAILURE: {
return {
...state
}
}
default:
return {
...state
}
}
}
const persistConfig: PersistConfig<any> = {
key: 'ProductDetail',
whitelist: ['product'],
storage: AsyncStorage,
version: 1,
timeout: 0
};
export default persistReducer(persistConfig, productDetailReducer) as any;
Package I use:
"react-native": "0.61.4"
"react-redux": "^7.2.0"
"redux": "^4.0.5",
"redux-persist": "^6.0.0"
"redux-saga": "^1.1.3"
Does anyone have any solution? Thank a lot
It is not advised to use multiple stores but react redux connect can use a different store by providing a store prop when rendering the component that connect creates. Here is an example:
const { Provider, useSelector, connect } = ReactRedux;
const { createStore } = Redux;
const store1 = createStore((x) => x, {
message: 'store 1',
});
const store2 = createStore((x) => x, {
message: 'store 2',
});
const Messages = ({ message2 }) => {
//useSelector will use the prop value store from Provider
const message1 = useSelector((s) => s.message);
return (
<ul>
<li>{message1}</li>
<li>{message2}</li>
</ul>
);
};
//connect uses prop store={store2} or store={store1} when
// MessageContainer does not have a store prop
const MessageContainer = connect(({ message }) => ({
message2: message,
}))(Messages);
const App = () => {
return <MessageContainer store={store2} />;
};
ReactDOM.render(
<Provider store={store1}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>

MobX observable changes not updating components

I have a store in MobX that handles containing the cart, and adding items to it. However, when that data is updated the components don't re-render to suit the changes.
I tried using useEffect to set the total price, however when the cartData changes, the state is not updated. It does work on mount, though.
In another component, I call addItemToCart, and the console.warn function returns the proper data - but that data doesn't seem to be synced to the component. clearCart also seems to not work.
My stores are contained in one RootStore.
import React, {useState, useEffect} from 'react';
import {List, Text} from '#ui-kitten/components';
import {CartItem} from '_molecules/CartItem';
import {inject, observer} from 'mobx-react';
const CartList = (props) => {
const {cartData} = props.store.cartStore;
const [totalPrice, setTotalPrice] = useState(0);
const renderCartItem = ({item, index}) => (
<CartItem itemData={item} key={index} />
);
useEffect(() => {
setTotalPrice(cartData.reduce((a, v) => a + v.price * v.quantity, 0));
console.warn('cart changed!');
}, [cartData]);
return (
<>
<List
data={cartData}
renderItem={renderCartItem}
style={{backgroundColor: null, width: '100%'}}
/>
<Text style={{textAlign: 'right'}} category={'s1'}>
{`Total $${totalPrice}`}
</Text>
</>
);
};
export default inject('store')(observer(CartList));
import {decorate, observable, action} from 'mobx';
class CartStore {
cartData = [];
addItemToCart = (itemData) => {
this.cartData.push({
title: itemData.title,
price: itemData.price,
quantity: 1,
id: itemData.id,
});
console.warn(this.cartData);
};
clearCart = () => {
this.cartData = [];
};
}
decorate(CartStore, {
cartData: observable,
addItemToCart: action,
clearCart: action,
});
export default new CartStore();
I have solved the issue by replacing my addItemToCart action in my CartStore with this:
addItemToCart = (itemData) => {
this.cartData = [
...this.cartData,
{
title: itemData.title,
price: itemData.price,
quantity: 1,
id: itemData.id,
},
];
console.warn(this.cartData);
};

React Native - Cannot read property [PROPERTY] of undefined (ignite 2, reduxsauce)

I'm having trouble using redux in my react native app. I cannot call an action in my component. I get the following error:
This is my AuthRedux.js
import { createReducer, createActions } from 'reduxsauce'
import Immutable from 'seamless-immutable'
const { Types, Creators } = createActions({
login: ['email', 'password'],
logout: null
})
export const AuthTypes = Types
export default Creators
export const INITIAL_STATE = Immutable({
isLoggedIn: false,
email: null,
password: null
})
export const userLogin = (state, {email, password}) => {
return Object.assign({}, state, {
isLoggedIn: true
});//state.merge({ isLoggedIn: true, email, password})
}
export const userLogout = (state) => {
return state.merge({ isLoggedIn: false, email: null, password: null })
}
export const reducer = createReducer(INITIAL_STATE, {
[Types.USER_LOGIN]: userLogin,
[Types.USER_LOGOUT]: userLogout
})
And this is my component LoginScreen.js
import React, { Component } from 'react'
import { ScrollView, Text, KeyboardAvoidingView, TextInput, TouchableOpacity, Button } from 'react-native'
import { connect } from 'react-redux'
import { AuthActions } from '../Redux/AuthRedux'
// Add Actions - replace 'Your' with whatever your reducer is called :)
// import YourActions from '../Redux/YourRedux'
// Styles
import styles from './Styles/LoginScreenStyle'
class LoginScreen extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
opacity: 1.0,
isLoggedIn: false
}
}
render () {
return (
<ScrollView style={styles.container}>
<KeyboardAvoidingView behavior='position'>
<Text>LoginScreen</Text>
<TextInput style={{width: 100, backgroundColor: 'red', height: 50, marginTop: 10}} onChangeText={(text) => this.setState({email : text})}/>
<TextInput style={{width: 100, backgroundColor: 'yellow', height: 50, marginTop: 10}} onChangeText={(text) => this.setState({password : text})}/>
<Button title='Hola' onPress={this.onLogin}/>
</KeyboardAvoidingView>
</ScrollView>
)
}
onLogin = () => {
console.log(this.state.email);
this.setState({opacity: 0.5})
this.props.userLogin(this.state.email, this.state.password);
}
handleOnPress = () => {
this.setState({opacity: 0.5})
}
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.auth.isLoggedIn
}
}
const mapDispatchToProps = (dispatch) => {
return {
userLogin: (email, password) => dispatch(AuthActions.login(email, password))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen)
I'm trying to call userLogin function from the onPress button which is assigned in mapDispatchToProps. I also have my rootReducer configured like this:
const rootReducer = combineReducers({
nav: require('./NavigationRedux').reducer,
github: require('./GithubRedux').reducer,
search: require('./SearchRedux').reducer,
auth: require('./AuthRedux').reducer
})
And the store is also given to the Provider in App.js
class App extends Component {
render () {
return (
<Provider store={store}>
<RootContainer />
</Provider>
)
}
}
I don't know why login action is not detected.
Instead of import { AuthActions } from '../Redux/AuthRedux', do import AuthActions from '../Redux/AuthRedux', because you are doing export default on the actionCreators which are the ones that you want to import right now.
You can also do export const AuthActions = Creators where you are doing export default Creators, and you can keep your import statement the same way you have right now.