How to set state in component remotely on callback? - react-native

I am making a react native app which receives data from ws api on callback.
This function resides in api module and look like this:
ws.onmessage = (e) => { //callbnack to HOME
const request = JSON.parse(e.data)
...
}
I want to show received text on screen in Component. It has state with text field, and <Text> {this.state.screenText} </Text> element.
Both modules imported in Main screen component. How do I do this?

1. I think you should have to create a custom hook or context provider if you are not calling this API on the Main screen component
2. If you call this API function on the main screen then you should create a simple function eg. ABC(data) that will receive the data from API and pass it to the function that you are calling as a parameter. On the other side when API gives some responses call this function which you are sending as a parameter and pass the data to it.
e.g:
export const sendData = (e, ABC) => { //callbnack to HOME
const request = JSON.parse(e.data)
ABC(request)
...
}
On Mian Screen :
export {sendData} .....
function ABC(data){console.log(data)
}
sendData(e, abc)

Use state management library like redux, zustand, etc. Here's an example with zustand -
store.js
export const useMyStore = create(() => ({
text: 'hello',
}));
export const setText = (text) => useMyStore.setState({ text });
websocket.js
import { setText } from './store.js';
ws.onmessage = (e) => {
const request = JSON.parse(e.data)
setText(request.text);
...
}
Main.js
import { useMyStore } from './store.js';
function Main() {
const text = useMyStore((state) => state.text);
return (
<View>
<Text>{text} </Text>
</View>
);
}

Related

why chatMsgStore.addChatMsg(bdmsg) does not effect the store?

store.js
import {useLocalObservable} from "mobx-react-lite";
function chatStore() {
return {
chatmsg: [],
setChatMsg(arr) {
this.chatmsg = arr
},
addChatMsg(msg) {
this.chatmsg.push(msg)
}
}
}
export const useChatStore = () => useLocalObservable(chatStore)
app.js
const App = () => {
const chatMsgStore = useChatStore()
const AppFunctions = {chatMsgStore}
useEffect(() => {
socket.on(activechat.chatid, (bdmsg) => {
chatMsgStore.addChatMsg(bdmsg)
})
return () => {
socket.off(activechat.chatid)
}
}, [activechat, chatMsgStore.chatmsg])
return (
<>
<AppContext.Provider value={AppFunctions}>
.....................
</AppContext.Provider>
</>
)
}
export default App;
fetch.js
async function getChatMessages(url, body, userStore, chatMsgStore) {
........
chatMsgStore.setChatMsg(firstResData)
........
on app load i add a socket listener which deps are activechat and chatMsgStore.
this listener is dynamic and must be changed when deps change.
the only purpose of this listener is to add a msg to the store and re-render the observer component
deps :
activechat - non store state
chatMsgStore.chatmsg - store state
why chatMsgStore.addChatMsg(bdmsg) does not effect the store? so deeply nested components inside App.js is not re-rendering.
otherwise i have a function getChatMessages which i import from custom hook deep inside App.js which sets the messages. this func is not a child of App.js and it is not wrapped with observer chatMsgStore.setChatMsg(firstResData) works! i can set the message so the observer component will re-render
how to make this code in useeffect above work?
Your App component is not wrapped with observer HOC so it won't react to observable values changes.
Wrap it like that:
const App = observer(() => {
// ...
})
or when exporting:
export default observer(App)
More info in the docs
you should use autorun from mobx in order to set correctly the reactivity in useEffect, here is a link to the doc that explains why and how use it.
But I think that you should not put chatMsgStore.chatmsg inside the deps array because you're not using it inside the useEffect.
If you can provide a working example maybe we can help you further.

Ionic React - Navigate to a page when an FCM notification is tapped

I am implementing FCM notifications in an Ionic React application. I am having trouble navigating to another page to display the notification details.
I have created a FCMService class in my react App, and initialising this in the index.ts file.
// FCMService.ts
export default class FCMService {
public static Instance: FCMService;
private _store: Store<IAppState>;
constructor(store: Store<IAppState>) {
this._store = store;
}
public static Initalise(store: Store<IAppState>) {
if (!FCMService.Instance) {
FCMService.Instance = new FCMService(store);
FCMService.Instance.InitaliseFCM();
FCMService.Instance._store.subscribe(() => { console.log(store.getState()) });
} else {
console.debug("FCM service already intialised. Please use FCMService.Instance");
}
}
private InitaliseFCM() {
// Request permission to use push notifications
// iOS will prompt user and return if they granted permission or not
// Android will just grant without prompting
PushNotifications.requestPermission().then(result => {
console.log(result);
if (result.granted) {
// Register with Apple / Google to receive push via APNS/FCM
PushNotifications.register();
} else {
// Show some error
}
});
// On success, we should be able to receive notifications
PushNotifications.addListener('registration', (token: PushNotificationToken) => {
console.log(token);
localStorage.setItem("FCM_TOKEN", token.value);
}
);
// Some issue with our setup and push will not work
PushNotifications.addListener('registrationError',
(error: any) => {
console.log(error);
}
);
// Show us the notification payload if the app is open on our device
PushNotifications.addListener('pushNotificationReceived',
(notification: PushNotification) => {
console.log(notification);
let data = notification.notification.data as INotificationData;
}
);
// Method called when tapping on a notification
PushNotifications.addListener('pushNotificationActionPerformed',
(notification: PushNotificationActionPerformed) => {
console.log(notification);
let data = notification.notification.data as INotificationData;
this._store.dispatch(setNotificationActionCreator(data));
}
);
}
}
and then the index.ts
const store = configureStore();
interface MainProps {
store: Store<IAppState>;
}
FCMService.Initalise(store);
ReactDOM.render(<Provider store={store}><App /> </Provider>, document.getElementById('root'));
serviceWorker.unregister();
I even tried using the Redux store to save the notification on Tap - and then that would publish the notification change event (which might of worked - if I could access the useHistory() hook in the App.tsx file)
This was my attempt at navigating via Redux store in App.tsx
const App: React.FC<IProps> = ({ getCompanies, getUser, notification }) => {
console.log('app');
console.log(process.env);
const history = useHistory();
if(notification){
history.push(`/page/plot-position/{notification.id}`);
}
return (
<IonApp>
<IonReactRouter>
<IonSplitPane contentId="main" when="false">
<Menu />
<IonRouterOutlet id="main">
<Route path="/login" component={LoginPage} exact />
<PrivateRoute path="/page/plot-position/:notificationId/" component={PlotPositionPage} exact />
<Redirect from="/" to="/login" exact />
</IonRouterOutlet>
</IonSplitPane>
</IonReactRouter>
</IonApp>
);
};
const mapStateToProps = (store: IAppState) => {
return {
user: store.user.user as UserDTO,
notification: store.notificationState.notification
};
};
const mapDispatchToProps = (dispatch: any) => {
return {
getCompanies: () => dispatch(getCompaniesStartActionCreator()),
getUser: () => dispatch(getUserStartActionCreator())
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
It looks like your navigation works, but you're having trouble passing the notification object through to the page? You can pass the object through history state.
To access the useHistory hook you would need to make your FCMService a custom hook.
const useFCMService = (): void => {
const history = useHistory();
React.useEffect(() => {
// Method called when tapping on a notification
PushNotifications.addListener('pushNotificationActionPerformed',
(action: PushNotificationActionPerformed) => {
const notification = action.notification.data as INotificationData;
history.push({ pathname: '/page/plot-position/', state: { notification } });
}
);
}, []);
}
And then include your useFCMService custom hook in your App component.
const App: React.FC<IProps> = ({ getCompanies, getUser }) => {
useFCMService();
...
};
Deep linking provides us a way to do this: Using both an action to open the application and an action at opening the application we can enroute the user to the correct destination.
Opening the application
Here we will create an action to open the url when the user taps on the push notification; to do this less use a listener:
const {PushNotifications, App} = Plugins
***
PushNotifications.addListener(
"pushNotificationActionPerformed",
(notification: PushNotificationActionPerformed) =>{
const data = notification.notification.data;
if (data.packageNumber) App.openUrl({url: `com.company.appname://tabs/package-details/${data.packageNumber}`})
else App.openUrl({url:'/tabs'})
}
)
com.company.app:// is of capital importance since the app must reach the application must reach an existing given url, otherwise the following action(catching the url) won't be triggers since it waits a complete true from the App.openUrl function; as we are opening an internal url, this must begin with the apps given name in the capacitor config page(see the following example where we can realize how use the local url).
In this way we are adding a function to open the application in an specific route.
Redirecting the user
Here, we will complete the application's part from the deep linking tutorial: we create a new listener component who handles the appOpenUrl events and redirects to the user and we will put it on the main App file inside of its respective IonRouter:
const AppUrlListener: React.FC<any> = () => {
let history = useHistory();
useEffect(() => {
App.addListener('appUrlOpen', (data: any) => {
const slug = data.url.split(':/').pop();
if (slug) {
history.push(slug);
}
});
}, []);
return null;
};
Don't forget the route in router must begin with /, and since the application url contains :/, we split the url here and we get the second part, the slug; we push it on the history, triggering the router and getting the normal behaviour when you entering in a new route.
We will add this component inside of the router:
<IonReactRouter>
<IonSplitPane contentId="main">
<Menu />
<AppUrlListener />
<IonRouterOutlet id="main">
Now, the application will be listening the appOpenUrl event, and when it gets a new of this events, it will push the gotten url to the history, redirecting the user to that route.

How to mock React Functionnal Component method?

I don't found a lot of jest&enzyme mock/spy example when react components are functions instead of class...
Since it looks impossible to spy on functionnal components method with "jest.spyOn" like it's possible to do with class components, is there a better or other way than passing method reference in props like my example below ?
it('click on connect button', () => {
let handleConnect = jest.fn();
let shallowLogin = shallow(<Login handleConnect={handleConnect} />);
//const spy = jest.spyOn(shallowLogin.instance(), 'handleConnect');
//Impossible to spyOn like this ?
let button = shallowLogin.find('Styled(Button)');
button.simulate('press');
expect(handleConnect).toHaveBeenCalledTimes(1);
});
I hate to do this because it's intrusive on the component code and it force the change stuff inside for the tests...
Component example :
import useForm from 'react-hook-form';
export default function Login(props) {
const {register, handleSubmit, setValue} = useForm();
const onSubmit = data => {
console.log('data: ', data);
if (FormValidation(data, "login")) {
props.navigation.navigate('App');
}
};
return (
...
<Button style={styles.button} rounded large onPress={handleSubmit(onSubmit)}><Text
style={styles.buttonText}>{locale.t('loginScreen.CONNECT')}</Text></Button>
...
);
}

How to set config show/hide refresh button on AppBar

Click to see image
Button refresh on AppBar is not refresh on page Dashboard because I just use Component Card but work on page using component List or Datagrid, so I want to config show/hide refresh button on AppBar or how to fix it work for page not use component List or Datagrid.
Sorry I'm not strong in English.
You'll have to fetch some data from the react-admin state for it to work. Indeed, the refresh button just trigger the refreshView action which update the state.admin.ui.viewVersion key of the the react-admin redux state. This key is a simple counter. Internally, we use this counter to check whether we must update some components data. Here is a simple example of a connected Dashboard which can do things when refreshed:
import React, { Component } from "react";
import { connect } from "react-redux";
class Dashboard extends Component {
componentDidMount() {
this.doOnMountAndWhenRefreshed();
}
componentDidUpdate(prevProps) {
if (prevProps.views !== this.props.views) {
this.doOnMountAndWhenRefreshed();
}
}
doOnMountAndWhenRefreshed = () => {
// This is where you do update your component:
// - Make API requests
// - Fetch data from the react-admin store, etc.
};
render() {
const { views } = this.props;
return <div>Refreshed {views} times.</div>;
}
}
const mapStateToProps = state => ({ views: state.admin.ui.viewVersion });
export default connect(
mapStateToProps,
{}
)(Dashboard);
You can see it working in this codesandbox
Edit for newer version of react-admin
import { useVersion } from 'react-admin';
const Dashboard = () => {
const version = useVersion();
return <div>Refreshed {version} times.</div>;
}
In react-admin 4.x I managed to get the desired behaviour like this:
import React from 'react'
import { useQuery } from 'react-query'
const noop = async () => new Date().valueOf()
export const MyDashboard = () => {
const { data } = useQuery('myDashboard', noop)
return (
<div>Last refreshed at {data}</div>
)
}
export default MyDashboard
Note how data represents the value returned by noop().
That way, whenever the user presses the refresh icon in the AppBar, the component is re-rendered.

How to use a custom reducer's state as a permanent filter in a <List>?

I have a custom reducer and a connected component to change its state. Now I'd like to use this state as a permanent filter on List elements.
I understand the List elements are connected to the redux-state, so I hope I'm able to access it through the List component's props, but couldn't find a way how to do that.
The List component is connected but not yours.
import { connect } from "react-redux";
const MyList = ({ is_published, ...props }) => (
<List {...props} filter={{ is_published }}>
</List>
);
const mapStateToProps = state => ({
is_published: state.myCustomReducer.is_published,
});
export default connect(mapStateToProps, undefined)(MyList);
Edit:
Just found out we don't update data when this prop change. This is a bug and you can open an issue about it.
In the mean time, here's a workaround:
Create a custom saga listening to whatever action you use alongside your custom reducer (I'll call it SET_IS_PUBLISHED for my example). This custom saga should put the changeListParams action creator from react-admin with your filter.
It will probably looks like this (not tested):
import { takeEvery, put, select } from 'redux-saga/effects'
import { changeListParams } from 'react-admin'
import { SET_IS_PUBLISHED } from './isPublished'
const getCurrentListParams = (state, resource) => {
const resourceState = state.admin.resources[resource]
return resourceState.list.params
}
function handleSetPublished({ payload }) {
const currentParams = yield select(getCurrentListParams)
const newParams = {
// Keep the current params
...currentParams,
// Override the filter
filter: {
// Keep the current filter
...currentParams.filter,
// Only override the is_published
is_published: payload
}
}
// Dispatch the action for the `posts` resource
yield put(changeListParams('posts', newParams))
}
export default function* () {
yield takeEvery(SET_IS_PUBLISHED, handleSetPublished)
}
just to bring this into 2021, you can use the useSelector redux hook to get hold of your custom state:
import { useSelector } from 'react-redux';
const MyCustomThing = (props) => {
const is_published = useSelector(state => state.customState.is_published);
}
For completeness, react-admin provides a customReducers prop to its <Admin> component so you can extend the redux state with your custom values:
const customStateReducer = (customState = { is_published: false }, { type, payload }) => {
if (type === 'IS_PUBLISHED') customState.is_published = payload.is_published;
return customState;
}
<Admin customReducers={{ customState: customStateReducer }} ...>
etc