I am using react, redux and redux-saga. Now the thing is i want to track all the user interactions with the form such as onfocus timestamp, what he enters and when he submits. I want one common component that should do all the things like above for me. How to do this. Thanks in advance
I would disagree that this a perfect use case for a middleware. The tracking is a side-effect of user interaction and not really related to redux, as sending off analytics data does not affect the application state.
You probably want some kind of HOC <TrackableInput> which has event handlers for onChange, onFocus, etc. and each of these handlers fires analytics requests, as a side effect, before dispatching an action like INPUT_CHANGE_VALUE. Separate the concerns of tracking and managing application state.
const trackFormEvent = (type, value = null) => {
console.log('Track form event of type', type, 'with value:', value);
// Do actual tracking request in here.
};
class TrackableInput extends React.Component {
onChange = (changeEvent) => {
const { value } = changeEvent.target;
trackFormEvent('change', value);
this.props.onChange(value);
};
onFocus = (focusEvent) => {
trackFormEvent('focus');
this.props.onFocus(focusEvent);
};
onBlur = (blurEvent) => {
trackFormEvent('blur');
this.props.onBlur(blurEvent);
};
render() {
return (
<input
{...this.props}
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
);
}
}
If you want to track all the action triggered as a result of user actions. Example when user submits the form. You want to log "MY_FORM_ACTION/FORM_SUBMIT" and "MY_FORM_ACTION/FORM_SUBMIT_SUCCESS" or "MY_FORM_ACTION/FORM_SUBMIT_FAILURE" as well. Use middleware.
You can type all your form actions something like "MY_FORM_ACTION/ACTION_NAME"
and write a simple middleware like below
const FormLogger = store => next => action => {
if(action.type.includes('MY_FORM_ACTION/')){
// log your action here
}
next(action)
}
Then in the If condition you can send them to a server or just log it. This middleware will capture all form actions which have "MY_FORM_ACTION/". Add this middleware to your redux just like saga or thunk middleware and it will work!
else You can have a TracMe Component like below.
Const TrackMe = Component => props =>{
const {onChange, onClick, onBlur, ...rest} = props;
const logChange = (e) =>{
//log Change
if(onChange){onChange(e)
}
const logClick = (e) =>{
//log Click
if(onClick){onClick(e)
}
const logBlur = (e) =>{
//log blur
if(onBlur){onBlur(e)
}
return (<Component
onChange={logChange}
onClick={logClick}
onBlur={logBlur}
{...rest}/>)
}
Now if your component is <Input {…props } /> just wrap it with TrackMe(<Input {…props } />
Related
I have been trying to learn React-query but can't seem to trigger requests with my onSubmit event. Right now the code is sending the request with "washington" as the default parameter and printing it to the screen, and a new request also triggers with the onBlur event, and fetch the data if the city typed is valid.
The thing is that wish I could move this logic to the submit() function, treat the data on the input and only if the data is valid, proceed to make the request. This is the stackblitz where I reproduced the problem with a free apiKey: StackBlitz
This is the code:
import React, { useState } from 'react';
import { useQuery } from 'react-query';
import axios from 'axios';
const Fetch = async city => {
let apiKey = '91b5ff77e9e7d1985a6c80bbbb3b2034';
const { data } = await axios.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`
);
return data;
};
const Weather = () => {
const [city, setCity] = useState('washington');
const { data, error } = useQuery(['temperature', city], () => Fetch(city));
const submit = () => {};
return (
<div>
<form onSubmit={submit}>
<input onBlur={e => setCity(e.target.value)} type="text" />
<button type="submit">send</button>
</form>
{!data ? null : <div>{data.main.temp}</div>}
</div>
);
};
export default Weather;
You can also call setCity in the onSubmit event of the form, as the onSubmit event gets the complete submitted form in the submit event:
<form
onSubmit={(event) => {
event.preventDefault();
const city = new FormData(event.currentTarget).get("city");
// do validation here
if (isValid(city)) {
setCity(city)
}
>
<input name="city" type="text" />
<button type="submit">send</button>
</form>
make sure to give your input a name so that you can grab it from the form submit event.
You can use useMutation hooks. As what the documentation said mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, React Query exports a useMutation hook.. This hooks will return an object that gives you a mutation function that you can use to trigger request based on user interactions.
const { mutate: renamedMutationFunction } = useMutation(newTodo => axios.post('/todos', newTodo)).
Then somewhere in your code, you can do:
const handleClick = () => { renamedMutationFunction(); //invoking the mutation }
EDIT
see #TkDodo answer for better solution. You can basically just re-set the city, and react-query will automatically refetch the data.
I am having issues detecting state change from my Redux reducer in a React App. When I change the state within one component, the other component in the app does not receive the update without the component being loaded again or refreshed. Consider the following components:
Component1.js
const Component1 = ({
getFromDB,
updateDB,
db_stuff: { db_stuff }
}) => {
const id = some-id;
useEffect(() => {
getFromDB(id);
updateDB(id, { data: someUpdate });
}, [id]);
// this updates the database and dispatches new state change
const handleUpdateDB = () => {
updateDB(id, { data: someUpdate });
};
return (
<Fragment>
<Button
onClick={handleUpdateDB}
>
Update DB
</Button>
</Fragment>
)
};
Component1.propTypes = {
getFromDB: PropTypes.func.isRequired,
updateDB: PropTypes.func.isRequired,
db_stuff: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
db_stuff: state.db_stuff
});
export default connect(mapStateToProps, {
updateDB,
getFromDB
})(Component1);
In the above component, when the button is click, the action is called to write to the database and change the app state. Within another component I have the state but it is not detecting the state change. Below is the contents of Component2:
Component2
const Component2 = ({
getFromDB,
db_stuff: { db_stuff }
}) => {
const id = some-id;
const [openAlert, setOpenAlert] = useState(false);
const [redirect, setRedirect] = useState(false);
useEffect(() => {
getFromDB(id);
}, [id]);
const handleNextPageClick = () => {
// This is were the issue is I believe but I have included the other code in case it is needed
// I have tried removing getFromDB(id) but it doesn't help
// I have tried making the function async and using await getFromDB(id) but this doesn't work either
getFromDB(id);
if (db_stuff && db_stuff.data <= someOtherData) {
setOpenAlert(true);
} else if (db_stuff && db_stuff.data > someOtherData) {
setRedirect(true);
}
};
const handleAlertClose = () => {
setOpenAlert(false);
};
return (
<Fragment>
//... other components
<Button
onClick={handleNextPageClick}
>
Next
</Button>
<Snackbar
open={openAlert}
autoHideDuration={3000}
onClose={handleAlertClose}
>
<Alert onClose={handleAlertClose} severity="info">
Please wait
</Alert>
</Snackbar>
{redirect ? <Redirect to={`/AnotherComponent`} /> : null}
</Fragment>
);
};
Component2.propTypes = {
getFromDB: PropTypes.func.isRequired,
db_stuff: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
db_stuff: state.db_stuff
});
export default connect(mapStateToProps, {
getFromDB
})(Component2);
So when the user in Component2 clicks next they will get an alert notifying them to please wait for the user in Component1 to continue. When the user in Component1 clicks next, the database and state is updated. But when the user clicks the button they are notified one more time. If they click the button again, then they are allowed to continue. So the state is only updating after an event occurs in the component (user clicking next page updates the state). I want the state to update in Component2 once there is a state change made from Component1. As of now the button has to be clicked twice.
I have seen similar issues but none of the solutions have worked. Any help or guidance is greatly appreciated. Thank you in advance.
So the app runs on two separate machines? Then you expect the update that one user sends to reach the second user. But you use a GET request to get the current state once the component loads. There is no mechanism that subscribes to any state changes from the remote api that happen later. So only a refresh triggers the app to get the updated state.
To get instant updates a Websocket implementation could provide this instant update. But it requires a complete different setup. For now you may reside to polling the remote api periodically. Say every few seconds.
I have some cards in my application that can lead to another pages through clicks. So I have a main component that contains a button like this:
function MainComponent(props) {
.
.
.
const handleClick = (key) => {
history.push("/exampleurl/" + key);
};
Then according to the key passed, I have to make a request that gives me some information required to display it. As default I have my initial state as null, and when it completes the request, it changes to the object I got. But as soon as I click on the card, I get the re-render error.
function MyComponent(props) {
let { key } = useParams();
const [myObject, setMyObject] = React.useState(null)
useEffect(() => {
axios.get('/myendpoint/' + key).then( response => {
let myObject = response.data
setMyObject(myObject)
})
}, [key])
I suppose that the solution is avoiding the key value to update when it changes the state. But i am not finding the solution to this trouble.
Edit: The route that leads to the components:
<Route path="/inbox">
<MainComponent />
</Route>
<Route path="/exampleurl/:key">
<NewComponent />
</Route>
I think the problem is related to the handleClick function.
Every time this method is called, you push a new entry to the history stack. Which analyze your defined routes and render the linked component. In your case, it is the same component, but I am not sure if the router is capable to determine it, therefore I would expect a re-render.
Maybe a solution would be to include another state which is responsible to inform the component of the current obj being displayed on the screen. So key will be responsible only for the route parameter and this new state will be responsible for the internal navigation.
function MyComponent(props) {
let { key } = useParams();
const [myObject, setMyObject] = React.useState(null)
const [displayedObj, setDisplayedObj] = React.useState('');
useEffect(() => {
axios.get('/myendpoint/' + key).then( response => {
let myObject = response.data
setMyObject(myObject)
setDisplayedObj(key)
})
}, [key, displayedObj]) // we listen for displayedObj too
and then in the handleClick we update this new state. This will trigger the useEffect and therefore update the myObject state to the new value:
const handleClick = (key) => {
setDisplayedObj(key);
// This will trigger the useEffect and refresh
// the data displayed without reloading the page
};
This function component has a template method that calls onChangeHandler, which accepts a select value and updates state. The problem is, state does not update until after the render method is called a second time, which means the value of selected option is one step ahead of the state value of selectedRouteName.
I know there are lifecycle methods in class components that I could use to force a state update, but I would like to keep this a function component, if possible.
As noted in the code, the logged state of selectedRouteDirection is one value behind the selected option. How can I force the state to update to the correct value in a functional component?
This question is not the same as similarly named question because my question asks about the actual implementation in my use case, not whether it is possible.
import React, { useState, Fragment, useEffect } from 'react';
const parser = require('xml-js');
const RouteSelect = props => {
const { routes } = props;
const [selectedRouteName, setRouteName] = useState('');
const [selectedRouteDirection, setRouteDirection] = useState('');
//console.log(routes);
const onChangeHandler = event => {
setRouteName({ name: event.target.value });
if(selectedRouteName.name) {
getRouteDirection();
}
}
/*
useEffect(() => {
if(selectedRouteName) {
getRouteDirection();
}
}); */
const getRouteDirection = () => {
const filteredRoute = routes.filter(route => route.Description._text === selectedRouteName.name);
const num = filteredRoute[0].Route._text;
let directions = [];
fetch(`https://svc.metrotransit.org/NexTrip/Directions/${num}`)
.then(response => {
return response.text();
}).then(response => {
return JSON.parse(parser.xml2json(response, {compact: true, spaces: 4}));
}).then(response => {
directions = response.ArrayOfTextValuePair.TextValuePair;
// console.log(directions);
setRouteDirection(directions);
})
.catch(error => {
console.log(error);
});
console.log(selectedRouteDirection); // This logged state is one value behind the selected option
}
const routeOptions = routes.map(route => <option key={route.Route._text}>{route.Description._text}</option>);
return (
<Fragment>
<select onChange={onChangeHandler}>
{routeOptions}
</select>
</Fragment>
);
};
export default RouteSelect;
Well, actually.. even though I still think effects are the right way to go.. your console.log is in the wrong place.. fetch is asynchronous and your console.log is right after the fetch instruction.
As #Bernardo states.. setState is also asynchronous
so at the time when your calling getRouteDirection();, selectedRouteName might still have the previous state.
So to make getRouteDirection(); trigger after the state was set.
You can use the effect and pass selectedRouteName as second parameter (Which is actually an optimization, so the effect only triggers if selectedRouteName has changed)
So this should do the trick:
useEffect(() => {
getRouteDirection();
}, [selectedRouteName]);
But tbh.. if you can provide a Stackblitz or similar, where you can reproduce the problem. We can definitely help you better.
setState is asynchronous! Many times React will look like it changes the state of your component in a synchronous way, but is not that way.
I have a component which builds onto the Select component from Ant Design https://ant.design/components/select/
<SomeComponent
onSelect = { this.props.handleSelect }
onDeselect = { this.props.handleDeselect }
selectionList = { valuesList }
value = { values }/>
onSelect triggeres the action this.props.handleSelect
export function handleSelect(value) {
return dispatch => {
dispatch(actionCreator(HANDLE_SELECT, value));
}
}
That actions goes into the reducer
case HANDLE_SELECT: {
const newValues = value_select(state, action);
return {
...state,
find: {
...state.a,
values: newValues
}
}
}
Finally, value_select is called to do all the magic
export const value_select = function(state, action) {
...
const newData = {
XYZ: action.payload
}
return newData
}
This brings me to my question.
Is it possible to send further metadata with the action? Imagine I use the component <SomeComponent.../> several times. I would not know which of the rendered components triggered the action when the onSelect is fired.
If I want to process the information in value_select = function(state, action) {... later, I want to know which component caused the action to process my data properly. I need to set XYZ in value_select() dynamically, depending on which <SomeComponent.../> caused the action. action.payload only gives me what is saved in value in <SomeComponent.../>, nothing more.
Is there a way to send some more information with the onSelect or is that bad practice and I would need an action for each component <SomeComponent.../> anyway?
Absolutely. It's your action and your reducer, you can attach any information you want to it.
The most common approach for structuring an action is the Flux Standard Action approach, which expects your actions to look like {type, payload, meta, error} but it's really up to you what you put into your actions.
For some more ideas, you might want to read through the Structuring Reducers - Reusing Reducer Logic section of the Redux docs.