Preact doesn't show the value inside the list tag - preact

In this Preact example, I'm trying to map over items array and print the result inside the li tag.
But the li is always empty with no error in console.
What am I doing wrong?
import { h } from '../preact.js'
import { useState, useEffect } from 'preact/hooks'
function mySeachList ({
items
}) {
console.log(items)
const [searchList, setSearchList] = useState(items)
useEffect(() => {
setSearchList(items)
}, [])
return h('ul.test-class', searchList.map((item, index) => {
h('li', null, item.keywords)
}))
}
export default mySeachList

Related

how to ensure data is not null before passing it as props?

I have created a generic component to be used in 2 cases. 1 case when dealing with single piece of data the other when dealing with an array of data. I am trying to plot this data on a react leaflet map. Right now it works for my landingPage component which deals with the single plots of data.
Previously I had it also working for my array of data before I was passing props to generic component to render. The issue is when I try to load the page responsible for displaying the map with the array of data it returns null when the getInitPosition() function is called as the props data seems to be null when component is rendered but not null after it, I checked this through logging to console. I am confused as to how it works in the single component and not the array of data component as the calls to retrieve the data are very similar. Can anyone see where I am going wrong. It seems to be that although my polyineArray is set with correct values I then print out the polylines state to check if it is set after the call to setPolylines(polylineArray) but it seems to be empty and I do not know why?
Map array of data component:
import react from 'react';
import { useState, useEffect} from 'react';
import { MapContainer, TileLayer, Popup, Polyline} from 'react-leaflet';
import axios from 'axios';
import polyline from '#mapbox/polyline';
import MapComp from './MapComp';
function Mapp() {
const [activities, setActivities] = useState([]);
const [polylines, setPolylines] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setActivitieData();
}, [])
useEffect(() => {
if(activities.length) {
setPolylineArray();
setIsLoading(false);
}
}, [activities])
const getActivityData = async () => {
const response = await axios.get(
"http://localhost:8800/api/"
);
return response.data;
};
const setActivitieData = async () => {
const activityData = await getActivityData();
setActivities(activityData);
};
const setPolylineArray = () => {
const polylineArray = []
for(let i = 0; i < activities.length; i++) {
const polylineData = activities[i].map.summary_polyline;
const activityName = activities[i].name;
const activityType = activities[i].type
polylineArray.push({positions: polyline.decode(polylineData), name: activityName, activityType: activityType });
} // should push activity type as well
setPolylines(polylineArray);
//setIsLoading(false);
console.log("Polyline array = ", polylineArray);
console.log("polylines = ", polylines) // this seems to be empty????
}
return (
!isLoading ?
<MapComp activityData={{polylines}} />
: <div><p>Loading...</p></div>
)
}
export default Mapp;
single data plotting component which works:
import react from 'react';
import { useState, useEffect } from 'react';
import Card from './Card';
import axios from 'axios';
import polyline from '#mapbox/polyline';
function LandingPage() {
const [cardActivites, setCardActivities] = useState([]);
const [polylines, setPolylines] = useState([]);
const [cards, setCards] = useState([]);
useEffect(() => {
setActivitieData();
}, [])
useEffect(() => {
if(cardActivites.length) {
setPolylineArray();
activityCards();
}
}, [cardActivites])
const getActivityData = async () => {
const response = await axios.get(
"http://localhost:8800/api/"
);
return response.data;
};
const setActivitieData = async () => {
const activityData = await getActivityData();
setCardActivities(activityData);
};
const setPolylineArray = async () => {
const polylineArray = []
// right now activites refers to our button to show all activites
// we will eventually be using global state in redux
// for now we have a different state for card activties
for(let i = 0; i < cardActivites.length; i++) {
const polylineData = cardActivites[i].map.summary_polyline;
const activityName = cardActivites[i].name;
const activityType = cardActivites[i].type
polylineArray.push({positions: polyline.decode(polylineData), name: activityName, activityType: activityType });
} // should push activity type as well
setPolylines(polylineArray);
console.log("Polyline array = ", polylineArray);
}
const activityCards = async () => {
// set up polyline array
setCards(polylines.map((polyline, i) => {
return <Card key={i} activityData={{polyline}}/>
}))
console.log("cards = ", cards);
//return <Card activityData={{polylines}}/>
}
return (
<div>
<p>cards</p>
{cards}
</div>
)
}
export default LandingPage;
generic component: import react from 'react';
import { MapContainer, TileLayer, Popup, Polyline} from 'react-leaflet';
import polyline from '#mapbox/polyline';
import { useEffect, useState } from 'react';
function MapComp(props) {
function getInitPosition() {
console.log("props activity data = ", props);
if(!Array.isArray(props.activityData)) {
return [props.activityData.positions[0][0],props.activityData.positions[0][1]];
}
else {
return [props.activityData.poylines.positions[0][0],props.activityData.poylines.positions[0][1]];
}
}
return (
<MapContainer center={getInitPosition()} zoom={15} scrollWheelZoom={false} style={props.style}>
<TileLayer attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{!Array.isArray(props.activityData) && <Polyline positions={props.activityData.positions} >
<Popup>
<div>
<h2>{"Name: " + + props.activityData.name}</h2>
</div>
</Popup>
</Polyline>
}
{Array.isArray(props.activityData.polylines) && props.activityData.polylines.length > 1 &&
props.activityData.polylines.map((activity, idx) => (
<Polyline key={idx} positions={activity.positions}
<Popup>
<div>
<h2>{"Name: " + activity.name}</h2>
</div>
</Popup>
</Polyline>
))}
</MapContainer>
)
}
export default MapComp;
Can anyone see my issue why polylines isn't being set or how to ensure it is set before passing the data as props?

React 17.0.1 basic onChange is not updating values into state [duplicate]

I am trying to learn hooks and the useState method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in useState is not working for me, both with and without the spread syntax.
I have made an API on another PC that I am calling and fetching the data which I want to set into the state.
Here is my code:
<div id="root"></div>
<script type="text/babel" defer>
// import React, { useState, useEffect } from "react";
// import ReactDOM from "react-dom";
const { useState, useEffect } = React; // web-browser variant
const StateSelector = () => {
const initialValue = [
{
category: "",
photo: "",
description: "",
id: 0,
name: "",
rating: 0
}
];
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
// const response = await fetch("http://192.168.1.164:5000/movies/display");
// const json = await response.json();
// const result = json.data.result;
const result = [
{
category: "cat1",
description: "desc1",
id: "1546514491119",
name: "randomname2",
photo: null,
rating: "3"
},
{
category: "cat2",
description: "desc1",
id: "1546837819818",
name: "randomname1",
rating: "5"
}
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies);
} catch (e) {
console.error(e);
}
})();
}, []);
return <p>hello</p>;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);
</script>
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
<script src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
Neither setMovies(result) nor setMovies(...result) works.
I expect the result variable to be pushed into the movies array.
Much like .setState() in class components created by extending React.Component or React.PureComponent, the state update using the updater provided by useState hook is also asynchronous, and will not be reflected immediately.
Also, the main issue here is not just the asynchronous nature but the fact that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created. Now in the current state, the values within hooks are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not.
Even if you add a setTimeout the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout will still use the value from its previous closure and not the updated one.
setMovies(result);
console.log(movies) // movies here will not be updated
If you want to perform an action on state update, you need to use the useEffect hook, much like using componentDidUpdate in class components since the setter returned by useState doesn't have a callback pattern
useEffect(() => {
// action on update of movies
}, [movies]);
As far as the syntax to update state is concerned, setMovies(result) will replace the previous movies value in the state with those available from the async request.
However, if you want to merge the response with the previously existing values, you must use the callback syntax of state updation along with the correct use of spread syntax like
setMovies(prevMovies => ([...prevMovies, ...result]));
Additional details to the previous answer:
While React's setState is asynchronous (both classes and hooks), and it's tempting to use that fact to explain the observed behavior, it is not the reason why it happens.
TLDR: The reason is a closure scope around an immutable const value.
Solutions:
read the value in render function (not inside nested functions):
useEffect(() => { setMovies(result) }, [])
console.log(movies)
add the variable into dependencies (and use the react-hooks/exhaustive-deps eslint rule):
useEffect(() => { setMovies(result) }, [])
useEffect(() => { console.log(movies) }, [movies])
use a temporary variable:
useEffect(() => {
const newMovies = result
console.log(newMovies)
setMovies(newMovies)
}, [])
use a mutable reference (if we don't need a state and only want to remember the value - updating a ref doesn't trigger re-render):
const moviesRef = useRef(initialValue)
useEffect(() => {
moviesRef.current = result
console.log(moviesRef.current)
}, [])
Explanation why it happens:
If async was the only reason, it would be possible to await setState().
However, both props and state are assumed to be unchanging during 1 render.
Treat this.state as if it were immutable.
With hooks, this assumption is enhanced by using constant values with the const keyword:
const [state, setState] = useState('initial')
The value might be different between 2 renders, but remains a constant inside the render itself and inside any closures (functions that live longer even after render is finished, e.g. useEffect, event handlers, inside any Promise or setTimeout).
Consider following fake, but synchronous, React-like implementation:
// sync implementation:
let internalState
let renderAgain
const setState = (updateFn) => {
internalState = updateFn(internalState)
renderAgain()
}
const useState = (defaultState) => {
if (!internalState) {
internalState = defaultState
}
return [internalState, setState]
}
const render = (component, node) => {
const {html, handleClick} = component()
node.innerHTML = html
renderAgain = () => render(component, node)
return handleClick
}
// test:
const MyComponent = () => {
const [x, setX] = useState(1)
console.log('in render:', x) // ✅
const handleClick = () => {
setX(current => current + 1)
console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated
}
return {
html: `<button>${x}</button>`,
handleClick
}
}
const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>
I know that there are already very good answers. But I want to give another idea how to solve the same issue, and access the latest 'movie' state, using my module react-useStateRef.
As you understand by using React state you can render the page every time the state change. But by using React ref, you can always get the latest values.
So the module react-useStateRef let you use state's and ref's together. It's backward compatible with React.useState, so you can just replace the import statement
const { useEffect } = React
import { useState } from 'react-usestateref'
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
const result = [
{
id: "1546514491119",
},
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies.current); // will give you the latest results
} catch (e) {
console.error(e);
}
})();
}, []);
More information:
react-usestsateref
I just finished a rewrite with useReducer, following #kentcdobs article (ref below) which really gave me a solid result that suffers not one bit from these closure problems.
See: https://kentcdodds.com/blog/how-to-use-react-context-effectively
I condensed his readable boilerplate to my preferred level of DRYness -- reading his sandbox implementation will show you how it actually works.
import React from 'react'
// ref: https://kentcdodds.com/blog/how-to-use-react-context-effectively
const ApplicationDispatch = React.createContext()
const ApplicationContext = React.createContext()
function stateReducer(state, action) {
if (state.hasOwnProperty(action.type)) {
return { ...state, [action.type]: state[action.type] = action.newValue };
}
throw new Error(`Unhandled action type: ${action.type}`);
}
const initialState = {
keyCode: '',
testCode: '',
testMode: false,
phoneNumber: '',
resultCode: null,
mobileInfo: '',
configName: '',
appConfig: {},
};
function DispatchProvider({ children }) {
const [state, dispatch] = React.useReducer(stateReducer, initialState);
return (
<ApplicationDispatch.Provider value={dispatch}>
<ApplicationContext.Provider value={state}>
{children}
</ApplicationContext.Provider>
</ApplicationDispatch.Provider>
)
}
function useDispatchable(stateName) {
const context = React.useContext(ApplicationContext);
const dispatch = React.useContext(ApplicationDispatch);
return [context[stateName], newValue => dispatch({ type: stateName, newValue })];
}
function useKeyCode() { return useDispatchable('keyCode'); }
function useTestCode() { return useDispatchable('testCode'); }
function useTestMode() { return useDispatchable('testMode'); }
function usePhoneNumber() { return useDispatchable('phoneNumber'); }
function useResultCode() { return useDispatchable('resultCode'); }
function useMobileInfo() { return useDispatchable('mobileInfo'); }
function useConfigName() { return useDispatchable('configName'); }
function useAppConfig() { return useDispatchable('appConfig'); }
export {
DispatchProvider,
useKeyCode,
useTestCode,
useTestMode,
usePhoneNumber,
useResultCode,
useMobileInfo,
useConfigName,
useAppConfig,
}
With a usage similar to this:
import { useHistory } from "react-router-dom";
// https://react-bootstrap.github.io/components/alerts
import { Container, Row } from 'react-bootstrap';
import { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';
import { ControlSet } from '../../components/control-set';
import { keypadClass } from '../../utils/style-utils';
import { MaskedEntry } from '../../components/masked-entry';
import { Messaging } from '../../components/messaging';
import { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';
export const AltIdPage = () => {
const history = useHistory();
const [keyCode, setKeyCode] = useKeyCode();
const [phoneNumber, setPhoneNumber] = usePhoneNumber();
const [appConfig, setAppConfig] = useAppConfig();
const keyPressed = btn => {
const maxLen = appConfig.phoneNumberEntry.entryLen;
const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);
setPhoneNumber(newValue);
}
const doSubmit = () => {
history.push('s');
}
const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;
return (
<Container fluid className="text-center">
<Row>
<Messaging {...{ msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs }} />
</Row>
<Row>
<MaskedEntry {...{ ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber }} />
</Row>
<Row>
<SimpleKeypad {...{ keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass }} />
</Row>
<Row>
<ControlSet {...{ btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [{ text: 'Submit', click: doSubmit }] }} />
</Row>
</Container>
);
};
AltIdPage.propTypes = {};
Now everything persists smoothly everywhere across all my pages
React's useEffect has its own state/lifecycle. It's related to mutation of state, and it will not update the state until the effect is destroyed.
Just pass a single argument in parameters state or leave it a black array and it will work perfectly.
React.useEffect(() => {
console.log("effect");
(async () => {
try {
let result = await fetch("/query/countries");
const res = await result.json();
let result1 = await fetch("/query/projects");
const res1 = await result1.json();
let result11 = await fetch("/query/regions");
const res11 = await result11.json();
setData({
countries: res,
projects: res1,
regions: res11
});
} catch {}
})(data)
}, [setData])
# or use this
useEffect(() => {
(async () => {
try {
await Promise.all([
fetch("/query/countries").then((response) => response.json()),
fetch("/query/projects").then((response) => response.json()),
fetch("/query/regions").then((response) => response.json())
]).then(([country, project, region]) => {
// console.log(country, project, region);
setData({
countries: country,
projects: project,
regions: region
});
})
} catch {
console.log("data fetch error")
}
})()
}, [setData]);
Alternatively, you can try React.useRef() for instant change in the React hook.
const movies = React.useRef(null);
useEffect(() => {
movies.current='values';
console.log(movies.current)
}, [])
The closure is not the only reason.
Based on the source code of useState (simplified below). Seems to me the value is never assigned right away.
What happens is that an update action is queued when you invoke setValue. And after the schedule kicks in and only when you get to the next render, these update action then is applied to that state.
Which means even we don't have closure issue, react version of useState is not going to give you the new value right away. The new value doesn't even exist until next render.
function useState(initialState) {
let hook;
...
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState); // setValue HERE
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending);
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
function dispatchAction(queue, action) {
const update = {
action,
next: null
};
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
isMount = false;
workInProgressHook = fiber.memoizedState;
schedule();
}
There's also an article explaining the above in the similar way, https://dev.to/adamklein/we-don-t-know-how-react-state-hook-works-1lp8
I too was stuck with the same problem. As other answers above have clarified the error here, which is that useState is asynchronous and you are trying to use the value just after setState. It is not updating on the console.log() part because of the asynchronous nature of setState, it lets your further code to execute, while the value updating happens on the background. Thus you are getting the previous value. When the setState is completed on the background it will update the value and you will have access to that value on the next render.
If anyone is interested to understand this in detail. Here is a really good Conference talk on the topic.
https://www.youtube.com/watch?v=8aGhZQkoFbQ
I found this to be good. Instead of defining state (approach 1) as, example,
const initialValue = 1;
const [state,setState] = useState(initialValue)
Try this approach (approach 2),
const [state = initialValue,setState] = useState()
This resolved the rerender issue without using useEffect since we are not concerned with its internal closure approach with this case.
P.S.: If you are concerned with using old state for any use case then useState with useEffect needs to be used since it will need to have that state, so approach 1 shall be used in this situation.
If we have to update state only, then a better way can be if we use the push method to do so.
Here is my code. I want to store URLs from Firebase in state.
const [imageUrl, setImageUrl] = useState([]);
const [reload, setReload] = useState(0);
useEffect(() => {
if (reload === 4) {
downloadUrl1();
}
}, [reload]);
const downloadUrl = async () => {
setImages([]);
try {
for (let i = 0; i < images.length; i++) {
let url = await storage().ref(urls[i].path).getDownloadURL();
imageUrl.push(url);
setImageUrl([...imageUrl]);
console.log(url, 'check', urls.length, 'length', imageUrl.length);
}
}
catch (e) {
console.log(e);
}
};
const handleSubmit = async () => {
setReload(4);
await downloadUrl();
console.log(imageUrl);
console.log('post submitted');
};
This code works to put URLs in state as an array. This might also work for you.
With custom hooks from my library, you can wait for the state values to update:
useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise - is a promise wrapper around useEffect that can wait for updates and return a new value and possibly a previous one if the optional peekPrevValue argument is set to true.
(Live Demo)
import React, { useState, useEffect, useCallback } from "react";
import { useAsyncWatcher } from "use-async-effect2";
function TestComponent(props) {
const [counter, setCounter] = useState(0);
const [text, setText] = useState("");
const textWatcher = useAsyncWatcher(text);
useEffect(() => {
setText(`Counter: ${counter}`);
}, [counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
setCounter((counter) => counter + 1);
const updatedText = await textWatcher();
console.log(updatedText);
})();
}, []);
return (
<div className="component">
<div className="caption">useAsyncEffect demo</div>
<div>{counter}</div>
<button onClick={inc}>Inc counter</button>
</div>
);
}
export default TestComponent;
useAsyncDeepState is a deep state implementation (similar to this.setState (patchObject)) whose setter can return a promise synchronized with the internal effect. If the setter is called with no arguments, it does not change the state values, but simply subscribes to state updates. In this case, you can get the state value from anywhere inside your component, since function closures are no longer a hindrance.
(Live Demo)
import React, { useCallback, useEffect } from "react";
import { useAsyncDeepState } from "use-async-effect2";
function TestComponent(props) {
const [state, setState] = useAsyncDeepState({
counter: 0,
computedCounter: 0
});
useEffect(() => {
setState(({ counter }) => ({
computedCounter: counter * 2
}));
}, [state.counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await setState(({ counter }) => ({ counter: counter + 1 }));
console.log("computedCounter=", state.computedCounter);
})();
});
return (
<div className="component">
<div className="caption">useAsyncDeepState demo</div>
<div>state.counter : {state.counter}</div>
<div>state.computedCounter : {state.computedCounter}</div>
<button onClick={() => inc()}>Inc counter</button>
</div>
);
}
var [state,setState]=useState(defaultValue)
useEffect(()=>{
var updatedState
setState(currentState=>{ // Do not change the state by get the updated state
updateState=currentState
return currentState
})
alert(updateState) // the current state.
})
Without any addtional NPM package
//...
const BackendPageListing = () => {
const [ myData, setMyData] = useState( {
id: 1,
content: "abc"
})
const myFunction = ( x ) => {
setPagenateInfo({
...myData,
content: x
})
console.log(myData) // not reflecting change immediately
let myDataNew = {...myData, content: x };
console.log(myDataNew) // Reflecting change immediately
}
return (
<>
<button onClick={()=>{ myFunction("New Content")} }>Update MyData</button>
</>
)
Not saying to do this, but it isn't hard to do what the OP asked without useEffect.
Use a promise to resolve the new state in the body of the setter function:
const getState = <T>(
setState: React.Dispatch<React.SetStateAction<T>>
): Promise<T> => {
return new Promise((resolve) => {
setState((currentState: T) => {
resolve(currentState);
return currentState;
});
});
};
And this is how you use it (example shows the comparison between count and outOfSyncCount/syncCount in the UI rendering):
const App: React.FC = () => {
const [count, setCount] = useState(0);
const [outOfSyncCount, setOutOfSyncCount] = useState(0);
const [syncCount, setSyncCount] = useState(0);
const handleOnClick = async () => {
setCount(count + 1);
// Doesn't work
setOutOfSyncCount(count);
// Works
const newCount = await getState(setCount);
setSyncCount(newCount);
};
return (
<>
<h2>Count = {count}</h2>
<h2>Synced count = {syncCount}</h2>
<h2>Out of sync count = {outOfSyncCount}</h2>
<button onClick={handleOnClick}>Increment</button>
</>
);
};
Use the Background Timer library. It solved my problem.
const timeoutId = BackgroundTimer.setTimeout(() => {
// This will be executed once after 1 seconds
// even when the application is the background
console.log('tac');
}, 1000);
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;
Now you should see, that your code actually does work. What does not work is the console.log(movies). This is because movies points to the old state. If you move your console.log(movies) outside of useEffect, right above the return, you will see the updated movies object.

Infinite loop when pushing into array using UseState()?

I would like to store the data of a response into an array for reuse. I am using Axios for this. The issue I receive is that when I push into the array, it loops getBoroughAndId() and keeps pushing into the array. I can tell because I get a console.log() response where it keeps telling me I am making too many requests. Any advice? Thanks.
Edit: After taking another gander, I think the issue is that the id is always changing when running getBoroughAndId. I'm not sure how to stop this.
import React, { useEffect, useState } from 'react';
import { airtableApi } from '../services/api/airtable';
import { BoroughDay, BoroughGroup } from '../types/api';
const IndexPage = () => {
const [boroughs, setBoroughs] = useState<BoroughDay[]>([]);
const [boroughGroups, setBoroughGroups] = useState<BoroughGroup[]>([]);
const getBoroughsAndDays = () => {
airtableApi
.getBoroughsAndDays()
.then((response) => {
setBoroughs(response);
})
.catch(() => { });
};
const getBoroughAndId = (id: string) => {
airtableApi
.getBoroughAndId(id)
.then((response) => {
console.log(response);
setBoroughGroups(arr => [...arr, response])
return response;
})
.catch(() => { });
}
useEffect(() => {
getBoroughsAndDays()
}, [boroughGroups])
return (
<>
{boroughs.map((data) => {
getBoroughAndId(data.id);
})}
</>
)
}
export default IndexPage
Here is my corrected code. It works a lot better now, with less nonsense and everything being done in the first function.
import React, { useEffect, useState } from 'react';
import { airtableApi } from '../services/api/airtable';
import { BoroughDay, BoroughGroup } from '../types/api';
const IndexPage = () => {
const [boroughs, setBoroughs] = useState<BoroughDay[]>([]);
const [boroughGroups, setBoroughGroups] = useState<BoroughGroup[]>([]);
const getBoroughsDays = () => {
airtableApi
.getBoroughsAndDays()
.then((response) => {
setBoroughs(response.records);
response.records.map((data) => {
setBoroughGroups(arr => [...arr, {id: data.id, "Borough": data.fields["Borough"]}])
})
})
.catch(() => { })
}
useEffect(() => {
getBoroughsDays();
}, [])
return (
<>
{boroughGroups.map(data => <div>{data.id} {data.Borough}</div>)}
</>
)
}
export default IndexPage
In order to tell you the mistake you are committing, I will tell you the whole flow of your program.
First of all when component mounts, useEffect will be called, which will call the getBoroughsAndDays function.
Note: boroughGroups in dependency array in useEffect is causing an infinite loop
This function (getBoroughsAndDays()) will update the value of boroughs(using setBoroughs)
Now since the state updated the function will re render, hence output will be shown on the screen
Now observe, here you are calling "getBoroughAndId(data.id)" function (inside map function), which is updating the value of boroughGroups(using setBoroughGroups)
Now since the value of boroughGroups have changed, the useEffect method will be called, which will again trigger the "getBoroughsandDays()" function, repeating the whole process again, so that is the reason, it is creating infinite loop.
Note: When any value inside dependency array changes useEffect will be called.
Solution:
I don't know what functionality you want to achieve but remove "boroughGroups" dependency from useEffect (In this way it will behave like componentDidMount).
You have a useEffect hook that updates state: boroughs whenever value of state: boroughGroups changes.
In the return statement, you iterate through boroughs and update boroughGroups.
Back to the first statement.
To stop this infinite loop, stop updating boroughGroups, that triggers useEffect everytime.
useEffect(() => {getBoroughsAndDays()}, []);

GSAP timeline needed on every page in Gatsby

My Gatsby site use the same GSAP timeline on every page, so I want to stay DRY and my idea is to include my timeline in my Layout component in that order.
But I don't know how to pass refs that I need between children and layout using forwardRef.
In short, I don't know how to handle the sectionsRef part between pages and layout.
sectionsRef is dependant of the page content (children) but is needed in the timeline living in layout.
How can I share sectionsRef between these two (I tried many things but always leading to errors)?
Here's a codesandbox without the refs in the Layout:
https://codesandbox.io/s/jolly-almeida-njt2e?file=/src/pages/index.js
And the sandbox with the refs in the layout:
https://codesandbox.io/s/pensive-varahamihira-tc45m?file=/src/pages/index.js
Here's a simplified version of my files :
Layout.js
export default function Layout({ children }) {
const containerRef = useRef(null);
const sectionsRef = useRef([]);
sectionsRef.current = [];
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
const scrollTimeline = gsap.timeline();
scrollTimeline.to(sectionsRef.current, {
x: () =>
`${-(
containerRef.current.scrollWidth -
document.documentElement.clientWidth
)}px`,
ease: 'none',
scrollTrigger: {
trigger: containerRef.current,
invalidateOnRefresh: true,
scrub: 0.5,
pin: true,
start: () => `top top`,
end: () =>
`+=${
containerRef.current.scrollWidth -
document.documentElement.clientWidth
}`,
},
});
}, [containerRef, sectionsRef]);
return (
<div className="slides-container" ref={containerRef}>
{children}
</div>
);
}
index.js (page)
import { graphql } from 'gatsby';
import React, { forwardRef } from 'react';
import SectionImage from '../components/sections/SectionImage';
import SectionIntro from '../components/sections/SectionIntro';
import SectionColumns from '../components/sections/SectionColumns';
const HomePage = ({ data: { home } }, sectionsRef) => {
const { sections } = home;
const addToRefs = (el) => {
if (el && !sectionsRef.current.includes(el)) {
sectionsRef.current.push(el);
}
};
return (
<>
{sections.map((section) => {
if (section.__typename === 'SanitySectionIntro') {
return (
<SectionIntro key={section.id} section={section} ref={addToRefs} />
);
}
if (section.__typename === 'SanitySectionImage') {
return (
<SectionImage key={section.id} section={section} ref={addToRefs} />
);
}
if (section.__typename === 'SanitySectionColumns') {
return (
<SectionColumns
key={section.id}
section={section}
ref={addToRefs}
/>
);
}
return '';
})}
</>
);
};
export default forwardRef(HomePage);
export const query = graphql`
query HomeQuery {
// ...
}
`;
Any clue greatly appreciated :)

angular 2 testing kendo-autocomplete

I'm trying to test a component that has kendo-autocomplete control. When the test is ruining the popup with the result doesn't show at all.
What do I need to do?
Below you have the code if you need any other information please let me know.
Component
import { Component, OnInit, Input, Output, Inject } from '#angular/core';
import { IFieldLookUpService } from 'app/services/ifield-look-up.service';
import { FieldLookUpValueResults } from 'app/models/field-look-up-result';
#Component({
selector: 'field-lookup',
templateUrl: './field-lookup.component.html',
styleUrls: ['./field-lookup.component.css']
})
export class FieldLookupComponent implements OnInit {
#Input() fieldId: number;
#Input() fieldName: string;
#Output() selectedValue: string;
private source: FieldLookUpValueResults;
public fieldLookUpValues: FieldLookUpValueResults;
constructor(#Inject('IFieldLookUpService') private fieldLookUpService: IFieldLookUpService) { }
ngOnInit() {
this.loadData();
}
handleFilter(value) {
this.fieldLookUpValues.results = this.source.results.filter((s) => s.text.toLowerCase().indexOf(value.toLowerCase()) !== -1);
}
private loadData() {
this.fieldLookUpService.getLookUpValues(this.fieldId, this.fieldName)
.subscribe(data => { this.source = data;
this.fieldLookUpValues = new FieldLookUpValueResults(this.source.header, null);
})
}
}
Component.html
<div *ngIf="fieldLookUpValues">
<kendo-autocomplete [data]="fieldLookUpValues.results" [valueField]="'text'" [suggest]="true" [value]="selectedValue" [filterable]="true" (filterChange)="handleFilter($event)">
<ng-template kendoAutoCompleteHeaderTemplate>
<strong>{{fieldLookUpValues.header}}</strong>
</ng-template>
</kendo-autocomplete>
</div>
spec
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { DebugElement } from '#angular/core';
import { By } from '#angular/platform-browser';
import { FieldLookupComponent } from './field-lookup.component';
import { FieldLookUpValueResults, FieldLookUpValue } from 'app/models/field-look-up-result';
import { IFieldLookUpService } from 'app/services/ifield-look-up.service';
import { Observable } from 'rxjs/Observable';
import { DropDownsModule } from '#progress/kendo-angular-dropdowns';
fdescribe('FieldLookupComponent', () => {
let component: FieldLookupComponent;
let fixture: ComponentFixture<FieldLookupComponent>;
let debugEl: DebugElement;
let mockFieldLookUpService;
let inputElement;
beforeEach(async(() => {
mockFieldLookUpService = jasmine.createSpyObj('mockFieldLookUpService', ['getLookUpValues']);
let mockData = new FieldLookUpValueResults('LookUp Values Result Header',
[
new FieldLookUpValue('LookUp Value 1', '1'),
new FieldLookUpValue('LookUp Value 2', '2'),
]);
mockFieldLookUpService.getLookUpValues.and.returnValue(Observable.of(mockData));
TestBed.configureTestingModule({
declarations: [ FieldLookupComponent ],
imports: [
DropDownsModule
],
providers: [
{ provide: 'IFieldLookUpService', useFactory: () => mockFieldLookUpService },
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FieldLookupComponent);
component = fixture.componentInstance;
debugEl = fixture.debugElement;
fixture.detectChanges();
inputElement = debugEl.query(By.css('input')).nativeElement;
console.log(component);
});
fit('should be created', () => {
expect(component).toBeTruthy();
});
fit('should have the autocomplete input', () => {
expect(inputElement).toBeTruthy();
});
fdescribe('when character L is set in autocompelte box', () => {
let list: DebugElement;
let listItems: DebugElement[];
beforeEach(() => {
inputElement.value = 'L';
fixture.detectChanges();
list = debugEl.query(By.css('ul')).nativeElement;
listItems = list.queryAll(By.css('li'));
})
fit('should have the kend pop-up shown', () => {
expect(list).toBeTruthy();
});
});
});
I set the value 'L' to the autocomplete input and then I should see the popup but they are null (the list and ListItems)
inputElement.value = 'L';
fixture.detectChanges();
list = debugEl.query(By.css('ul')).nativeElement;
listItems = list.queryAll(By.css('li'));
The Popup component used in the AutoComplete (applicable to other Kendo components with Popup) is appended at the root component by default. In other words, the Popup is not part of the component tree.
For those interested in why this is so, read this Github issue
With those details in mind, you will need to use the AutoComplete instance and retrieve the Popup element from its popupRef property.
{{ autocomplete?.popupRef?.popupElement.nodeName }}
Here is a plunker that demonstrates this approach:
http://plnkr.co/edit/bQTmfBUT7r5z6wjt5MtL?p=preview
Please note that you will need to wait a tick in the tests in order to get the popupRef correctly.
P.S. IMHO, testing the rendered UL list is unneeded. The vendor providing the AutoComplete component has already tested the output items based on the passed [data] value. Considering this fact, I would just test the autocomplete.data property, which should be sufficient.
You can always add functional tests on top of that to ensure that the application you are building is working as a whole.

Resources