I'am testing the google maps autocomplete API in my react native app. I have a simple text input field which accepts a search term and based on that the app fetches data from the mentioned api. I have used redux to update the status of the locations in the app.
But after adding redux I can't type anything in the text input, It was working fine earlier. I don't see any error, so I'm helpless on how to resolve. I have attached the relevant codes. Helps would be appreciated.
App.js
const store = createStore(reducers, compose(applyMiddleware(thunk)));
const App = () => {
return (
<Provider store={store}>
<StatusBar barStyle="dark-content"/>
<SearchScreen/>
</Provider>
);
};
export default App;
Reducers.js
export default combineReducers({
locations
});
export default (locations = [], action) => {
switch (action.type) {
case actionTypes.FETCH_LOCATIONS:
return action.payload;
default:
return locations;
}
}
SearchScreen.js
const SearchScreen = () => {
const [from, setFrom] = useState("");
const dispatch = useDispatch();
const handleFrom = (str) => {
setFrom(str);
if (from.length >= 5)
dispatch(getLocations(from))
}
return (
<SafeAreaView>
<View style={styles.container}>
<TextInput
style={styles.textInput}
placeholder="from"
value={from}
onChangeText={(str) => handleFrom(str)}/>
<Text>{from}</Text>
</View>
</SafeAreaView>
)
};
export default SearchScreen;
getLocations
export const getLocations = (searchTerm) => async (dispatch) => {
try {
const {response} = await api.fetchLocations(searchTerm);
console.log(`response: ${JSON.stringify(response)}`);
// dispatch({type: actionTypes.FETCH_LOCATIONS, payload: response?.predictions});
} catch (e) {
console.error(e);
}
}
apis
const API = axios.create({
baseURL: 'https://google-maps-autocomplete-plus.p.rapidapi.com/autocomplete'
});
API.interceptors.request.use((req) => {
req.headers = {
'X-RapidAPI-Key': 'API-KEY',
'X-RapidAPI-Host': 'google-maps-autocomplete-plus.p.rapidapi.com'
};
return req;
});
export const fetchLocations = (query) => API.get(`?query=${query}&limit=5`);
async function always returns a Promise. So wait until it has a value before passing it to disptach.
export const getLocations = (searchTerm) => async {
try {
const { data } = await api.fetchLocations(searchTerm);
console.log(`response: ${JSON.stringify(data)}`);
return data;
} catch (e) {
console.error(e);
return [];
}
}
const handleFrom = (str) => {
setFrom(str);
if (str.length >= 5)
getLocations(str).then(data => dispatch(data, {});
}
Related
Description
I'm creating a state management tool for a small project, using mainly useSyncExternalStore from React, inspired by this video from Jack Herrington https://www.youtube.com/watch?v=ZKlXqrcBx88&ab_channel=JackHerrington.
But, I'm running into a pattern that doesn't look right, which is having to use 2 providers, one to create the state, and the other to initialise it.
The gist of the problem:
I have a property sessionId coming from an HTTP request. Saving it in my store wasn't an issue.
However, once I have a sessionId then all of my POST requests done with notifyBackend should have this sessionId in the request body. And I was able to achieve this requirement using the pattern above, but I don't like it.
Any idea how to make it better ?
Code
CreateStore.jsx (Not important, just providing the code in case)
export default function createStore(initialState) {
function useStoreData(): {
const store = useRef(initialState);
const subscribers = useRef(new Set());
return {
get: useCallback(() => store.current, []),
set: useCallback((value) => {
store.current = { ...store.current, ...value };
subscribers.current.forEach((callback) => callback());
}, []),
subscribe: useCallback((callback) => {
subscribers.current.add(callback);
return () => subscribers.current.delete(callback);
}, []),
};
}
const StoreContext = createContext(null);
function StoreProvider({ children }) {
return (
<StoreContext.Provider value={useStoreData()}>
{children}
</StoreContext.Provider>
);
}
function useStore(selector) {
const store = useContext(StoreContext);
const state = useSyncExternalStore(
store.subscribe,
() => selector(store.get()),
() => selector(initialState),
);
// [value, appendToStore]
return [state, store.set];
}
return {
StoreProvider,
useStore,
};
}
Creating the state
export const { StoreProvider, useStore } = createStore({
sessionId: "INITIAL",
notifyBackend: () => { },
});
index.jsx
<Router>
<StoreProvider>
<InitialisationProvider>
<App />
</InitialisationProvider>
</StoreProvider>
</Router
InitialisationContext.jsx
const InitialisationContext = createContext({});
export const InitializationProvider = ({ children }) {
const [sessionId, appendToStore] = useStore(store => store.session);
const notifyBackend = async({ data }) => {
const _data = {
...data,
sessionId,
};
try {
const result = await fetchPOST(data);
if (result.sessionId) {
appendToStore({ sessionId: result.sessionId });
} else if (result.otherProp) {
appendToStore({ otherProp: result.otherProp });
}
} catch (e) { }
};
useEffect(() => {
appendToStore({ notifyBackend });
}, [sessionId]);
return (
<InitialisationContext.Provider value={{}}>
{children}
</InitialisationContext.Provider>
);
}
I just tried out Zustand, and it's very similar to what I'm trying to achieve.
Feels like I'm trying to reinvent the wheel.
With Zustand:
main-store.js
import create from 'zustand';
export const useMainStore = create((set, get) => ({
sessionId: 'INITIAL',
otherProp: '',
notifyBackend: async ({ data }) => {
const _data = {
...data,
sessionId: get().sessionId,
};
try {
const result = await fetchPOST(data);
if (result.sessionId) {
set({ sessionId: result.sessionId });
} else if (result.otherProp) {
set({ otherProp: result.otherProp });
}
} catch (e) { }
},
}));
SomeComponent.jsx
export const SomeComponent() {
const sessionId = useMainStore(state => state.sessionId);
const notifyBackend = useMainStore(state => state.notifyBackend);
useEffect(() => {
if (sessionId === 'INITIAL') {
notifyBackend();
}
}, [sessionId]);
return <h1>Foo</h1>
};
This answer focuses on OPs approach to createStore(). After reading the question a few more times, I think there are bigger issues. I'll try to get to these and then extend the answer.
Your approach is too complicated.
First, the store is no hook! It lives completely outside of react. useSyncExternalStore and the two methods subscribe and getSnapshot are what integrates the store into react.
And as the store lives outside of react, you don't need a Context at all.
Just do const whatever = useSyncExternalStore(myStore.subscribe, myStore.getSnapshot);
Here my version of minimal createStore() basically a global/shared useState()
export function createStore(initialValue) {
// subscription
const listeners = new Set();
const subscribe = (callback) => {
listeners.add(callback);
return () => listeners.delete(callback);
}
const dispatch = () => {
for (const callback of listeners) callback();
}
// value management
let value = typeof initialValue === "function" ?
initialValue() :
initialValue;
// this is what useStore() will return.
const getSnapshot = () => [value, setState];
// the same logic as in `setState(newValue)` or `setState(prev => newValue)`
const setState = (arg) => {
let prev = value;
value = typeof arg === "function" ? arg(prev) : arg;
if (value !== prev) dispatch(); // only notify listener on actual change.
}
// returning just a custom hook
return () => useSyncExternalStore(subscribe, getSnapshot);
}
And the usage
export const useMyCustomStore = createStore({});
// ...
const [value, setValue] = useMyCustomStore();
I am trying make an "easy" weather app exercise, just get data from api and render it. I am using "google api map" to get the location from a post code to a latitude and longitud parameters so I can use those numbers and pass it to "open weather map" api to get the weather for that location.
It is working but with bugs...
First I used redux for "location" and "weather". Redux was working but useSelector() wasnt displaying the data properly.
Then I decide to make it easy, on "search" component I am calling an api an getting the location I need, I am storing it with redux and it is working, on "weatherFullDispaly" component I am calling an api for the "weather" details and just pass it as props for the children to render the data but they are not getting it.
The thing is, while the app is running, when I put a post code I get an error because the children are not receiving the data but, if I comment out the children on the parent component and then comment in again, all the data print perfect.
Any help please???
const WeatherFullDisplay = () => {
const [weatherDetails, setWeatherDetails] = useState();
const currentLocation = useSelector(getLocationData);
useEffect(() => {
getWeatherDetails();
}, []);
const getWeatherDetails = async () => {
const API_KEY = process.env.REACT_APP_OPEN_WEATHER_MAP_API_KEY;
const { lat, lng } = await currentLocation.results[0].geometry.location;
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lng}&exclude=minutely&units=metric&appid=${API_KEY}`
);
setWeatherDetails(response.data);
};
return (
<div className="weather-full-display-details">
<WeatherNow weatherDetails={weatherDetails} />
<HourlyWeather weatherDetails={weatherDetails} />
<FiveDaysWeather weatherDetails={weatherDetails} />
</div>
);
};
const FiveDaysWeather = ({ weatherDetails }) => {
const displayDailyWeather = () => {
const daysToShow = [
weatherDetails.daily[1],
weatherDetails.daily[2],
weatherDetails.daily[3],
weatherDetails.daily[4],
weatherDetails.daily[5],
];
return daysToShow.map((day, i) => {
return (
<WeatherSingleCard
key={i}
typeOfCard="daily"
weekDay={moment(day.dt * 1000).format("dddd")}
icon={day.weather[0].icon}
weather={day.weather[0].main}
temp={day.temp.day}
/>
);
});
};
return (
<div className="day-single-cards">{displayDailyWeather()}</div>
);
};
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
locationDetails: "",
};
const locationSlice = createSlice({
name: "location",
initialState,
reducers: {
setLocation: (state, action) => {
state.locationDetails = action.payload;
},
cleanLocation: (state) => {
state.locationDetails = ""
}
},
});
export const { setLocation, cleanLocation } = locationSlice.actions;
export const getLocationData = (state) => state.location.locationDetails;
export default locationSlice.reducer;
const SearchBar = () => {
const [postCode, setPostCode] = useState();
const [locationDetails, setLocationDetails] = useState();
const navigate = useNavigate();
const dispatch = useDispatch();
useEffect(() => {
getLocationDetails();
}, [postCode]);
const getLocationDetails = async () => {
const response = await axios.get(
"https://maps.googleapis.com/maps/api/geocode/json",
{
params: {
components: `country:ES|postal_code:${postCode}`,
region: "ES",
key: process.env.REACT_APP_GOOGLE_API_KEY,
},
}
);
setLocationDetails(response.data);
};
const handleSubmit = (e) => {
e.preventDefault();
dispatch(setLocation(locationDetails));
navigate("/detail-weather");
};
const handleChange = (e) => {
setPostCode(e.target.value);
};
I have an app with redux and redux toolkit.
I try to have a background call to refresh my notifications in the background but every time it is called the whole app gets refreshed.
In my Notification slice I have the following createAsyncThunk:
// Thunk is an async middleware for handling reducers
export const reloadNotifications = createAsyncThunk(
'notifications/reload',
async (userToken: string): Promise<Notification[]> => {
try {
const req = await axios.post(process.env.REACT_APP_GRAPHQL_ENDPOINT as string, {
query: myNotifications()
}, { headers: { "Authorization": `Bearer ${userToken}` } })
// Check data exists before pushing
if (req?.data?.data?.myNotification) {
return req.data.data?.myNotification as Notification[]
} else {
return []
}
} catch (error) {
return []
}
}
)
In my NotificationHeader component I have this:
export const NotificationHeader: React.FC<any> = () => {
const notifications = useSelector(s => s.notifications.notifications);
const [seenNotification] = useMutation(SEEN_NOTIFICATION);
const [location, setLocation] = useLocation();
const dispatch = useDispatch();
const auth = useSelector(s => s.auth);
const handleClick = (notification: Notification) => {
seenNotification({ variables: { notificationID: notification.id } }).then(async ({ data }) => {
setLocation(getUrlNotification(notification, auth.user.id))
}).catch((e: any) => {
setLocation(getUrlNotification(notification, auth.user.id))
})
}
useEffect(() => {
const timer = setTimeout(
() => {
// Only get notifications if i'm logged in
if (auth.isLogged) {
dispatch(reloadNotifications(auth.token))
}
}, 3000);
// This handles componentUnmount to clear the timer
return () => clearTimeout(timer);
});
return (
<Menu placement="bottom-start">
<MenuButton marginRight="10px" variant="secondaryAction" bg="brand.orange" color="brand.purple" as={Button}>
{(notifications && notifications.length > 0 && notifications.length < 10 &&
<SmallBadge content={notifications.length} />
)}
{(notifications && notifications.length > 9 &&
<SmallBadge content="9+" />
)}
<FontAwesomeIcon icon={faBell} />
</MenuButton>
<MenuList commandSpacing="sm" bg="brand.purple" color="brand.orange">
{(notifications && notifications.length > 0) ?
notifications.map(notif => (
<MenuItem key={`notif-${notif.id}`} maxH={20} _focus={{ bg: "brand.orange", color: "brand.purple" }} className="notificationItem">
{(!notif.isSeen) ? <Badge marginRight={2} size="sm" colorScheme="green">NEW</Badge> : undefined}
<Link href="#" onClick={() => handleClick(notif)}>{notif.title}</Link><Text marginLeft={4} marginRight={2} textAlign="right" flex="1" color="brand.gray" as="i" fontSize="xs">{moment(notif.createdAt).format(`DD MMM YYYY`)}</Text>
</MenuItem>
))
: (
<MenuItem isFocusable={false} textAlign="center" maxH={20} _focus={{ bg: "brand.orange", color: "brand.purple" }}>
You have no new notifications
</MenuItem>
)}
</MenuList>
</Menu >
);
}
However with this the interval causes a full refresh of the app even non child components.
I have also tried to add the following middleware to cause the notification interval to be triggered but this caused the full app to crash
export const updateNotificationsMiddleware: Middleware = api => next => action => {
const updateNotifications = async () => {
const { auth } = api.getState() as State;
api.dispatch({
type: 'notifications/reload',
payload: auth.token
});
setTimeout(updateNotifications, 3000);
};
updateNotifications();
return next(action);
};
How can I have a simple API call with redux that will refresh my state every x second without causing a full app refresh.
I have also tried the following from the answer below:
let initialized = false
export const updateNotificationsMiddleware: Middleware = api => next => action => {
const updateNotifications = async () => {
console.log('in middleware')
const { auth } = api.getState() as State;
api.dispatch({
type: 'notifications/reload',
payload: auth.token
});
setTimeout(updateNotifications, 3000);
};
if (!initialized){
initialized = true
updateNotifications();
}
return next(action);
};
I have then updated my thunk to reflect the following:
// Thunk is an async middleware for handling reducers
export const reloadNotifications = createAsyncThunk(
'notifications/reload',
async (userToken: string): Promise<Notification[]> => {
console.log('in action')
try {
const req = await axios.post(process.env.REACT_APP_GRAPHQL_ENDPOINT as string, {
query: myNotifications()
}, { headers: { "Authorization": `Bearer ${userToken}` } })
// Check data exists before pushing
if (req?.data?.data?.myNotification) {
return req.data.data?.myNotification as Notification[]
} else {
return []
}
} catch (error) {
return []
}
}
)
The middleware console.log is indeed shown every 3 second which is awesome but the action is still never called. The console.log does not appear once and the network request also does not get triggered.
Gave your middleware a re-read. You add a timer on every action happening, which probably causes your problem. I think getting it down to doing that only once should solve your problem:
let initialized = false
export const updateNotificationsMiddleware: Middleware = api => next => action => {
const updateNotifications = async () => {
const { auth } = api.getState() as State;
api.dispatch({
type: 'notifications/reload',
payload: auth.token
});
setTimeout(updateNotifications, 3000);
};
if (!initialized){
initialized = true
updateNotifications();
}
return next(action);
};
I was searching for a loading spinner for react+redux and came across the react-redux-spinner library. I included it in my project, added the reducer, called [pendingTask]: begin/end in my actions, added the Spinner component to render, but it just won't show at all, even though in the redux logs I can see that pending tasks in the store are incremented and decremented accordingly to the action called. Here is some of my code:
store:
const rootReducer = combineReducers({
pendingTasks: pendingTasksReducer
// other reducers
});
const store = createStore(rootReducer, /* middlewares */);
export default store;
actions
export const fetchData = params => {
const request = params => ({
type: 'FETCH_REQUEST',
[pendingTask]: begin,
payload: { params }
});
const success = data => ({
type: 'FETCH_SUCCESS',
[pendingTask]: end,
payload: { data }
});
const failure = error => ({
type: 'FETCH_FAILURE',
[pendingTask]: end,
payload: { error }
});
return async dispatch => {
dispatch(request(params));
try {
const res = await service.fetchData(params);
dispatch(success(res.data));
return res.data;
} catch (e) {
const msg = e.toString();
dispatch(failure(msg));
return Promise.reject(msg);
}
}
}
page
const Page = props => {
const { data } = props;
useEffect(() => {
async function fetchData(params) {
try {
await props.fetchData(params);
} catch (e) {
console.log(e);
}
}
fetchData(data.params);
}
return (
<div className="wrapper">
{
data.map(({ field1, field2 }, key) => ({
<div>{field1}: {field2}</div>
}));
}
</div>
);
};
const mapStateToProps = state => {
const { data } = state;
return { data };
};
const actionCreators = {
fetchData: actions.fetchData
};
export default connect(mapStateToProps, actionCreators)(Page);
app component
export const App = props => {
return (
<main className="App">
<Spinner config={{ trickeRate: 0.02 }} />
<Page/>
</main>
);
}
I've double-checked that I use the correct names for the store and for the actions, and they do fire up - but the spinner itself never gets rendered on the page at all, even though with each action the pendingTasks value change. What could I possibly do wrong or miss here? Infinitely grateful in advance for pointing out!
I am trying to reproduce something I was doing with Reactjs/ Redux/ redux-thunk:
Show a spinner (during loading time)
Retrieve information from remote server
display information and remove spinner
The approach was to use useReducer and useContext for simulating redux as explained in this tutorial. For the async part, I was relying on redux-thunk, but I don't know if there is any alternative to it for useReducer. Here is my code:
The component itself :
const SearchForm: React.FC<unknown> = () => {
const { dispatch } = React.useContext(context);
// Fetch information when clickin on button
const getAgentsInfo = (event: React.MouseEvent<HTMLElement>) => {
const fetchData:() => Promise<void> = async () => {
fetchAgentsInfoBegin(dispatch); //show the spinner
const users = await fetchAgentsInfo(); // retrieve info
fetchAgentsInfoSuccess(dispatch, users); // show info and remove spinner
};
fetchData();
}
return (
...
)
The data fetcher file :
export const fetchAgentsInfo:any = () => {
const data = await fetch('xxxx');
return await data.json();
};
The Actions files:
export const fetchAgentsInfoBegin = (dispatch:any) => {
return dispatch({ type: 'FETCH_AGENTS_INFO_BEGIN'});
};
export const fetchAgentsInfoSuccess = (dispatch:any, users:any) => {
return dispatch({
type: 'FETCH_AGENTS_INFO_SUCCESS',
payload: users,
});
};
export const fetchAgentsInfoFailure = (dispatch:any) => {
return dispatch({
type: 'FETCH_AGENTS_INFO_FAILURE'
})
};
And my store itself :
import React, { createContext, useReducer } from 'react';
import {
ContextArgs,
ContextState,
ContextAction
} from './types';
// Reducer for updating the store based on the 'action.type'
const Reducer = (state: ContextState, action: ContextAction) => {
switch (action.type) {
case 'FETCH_AGENTS_INFO_BEGIN':
return {
...state,
isLoading:true,
};
case 'FETCH_AGENTS_INFO_SUCCESS':
return {
...state,
isLoading:false,
agentsList: action.payload,
};
case 'FETCH_AGENTS_INFO_FAILURE':
return {
...state,
isLoading:false,
agentsList: [] };
default:
return state;
}
};
const Context = createContext({} as ContextArgs);
// Initial state for the store
const initialState = {
agentsList: [],
selectedAgentId: 0,
isLoading:false,
};
export const ContextProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
const value = { state, dispatch };
Context.displayName = 'Context';
return (
<Context.Provider value={value}>{children}</Context.Provider>
);
};
export default Context;
I tried to partially reuse logic from this article but the spinner is never displayed (data are properly retrieved and displayed).
Your help will be appreciated !
Thanks
I don't see anything in the code you posted that could cause the problem you describe, maybe do console.log in the reducer to see what happends.
I do have a suggestion to change the code and move logic out of the component and into the action by using a sort of thunk action and replacing magic strings with constants:
//action types
const BEGIN = 'BEGIN',
SUCCESS = 'SUCCESS';
//kind of thunk action (cannot have getState)
const getData = () => (dispatch) => {
dispatch({ type: BEGIN });
setTimeout(() => dispatch({ type: SUCCESS }), 2000);
};
const reducer = (state, { type }) => {
if (type === BEGIN) {
return { ...state, loading: true };
}
if (type === SUCCESS) {
return { ...state, loading: false };
}
return state;
};
const DataContext = React.createContext();
const DataProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, {
loading: false,
});
//redux-thunk action would receive getState but
// cannot do that because it'll change thunkDispatch
// when state changes and could cause problems when
// used in effects as a dependency
const thunkDispatch = React.useCallback(
(action) =>
typeof action === 'function'
? action(dispatch)
: action,
[]
);
return (
<DataContext.Provider
value={{ state, dispatch: thunkDispatch }}
>
{children}
</DataContext.Provider>
);
};
const App = () => {
const { state, dispatch } = React.useContext(DataContext);
return (
<div>
<button
onClick={() => dispatch(getData())}
disabled={state.loading}
>
get data
</button>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(
<DataProvider>
<App />
</DataProvider>,
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>
<div id="root"></div>