Dynamic and controlled component in react hooks - javascript

I want to dynamically generate controlled input components.
data is an array of object, which has following form:
data = [{obj1}, {obj2}, ...] // length not fixed
After data is loaded, it triggers useMemo and useEffect as below:
const [row, setRow] = useState([]);
const onContentChange = e => {
const currentRow = [...row]
currentRow[e.target.dataset.idx][e.target.className] = e.target.value
setRow(currentRow)
}
useEffect(() => {
const arr = []
data.forEach(obj => {
arr.push({
content: obj.content
})
})
setRow(arr)
}, [data])
const mData = useMemo(() => {
const retArr = []
data.forEach( (obj, i) => {
retArr.push({
content: (
<input
type="text"
className="content"
name={`content-${i}`}
id={`content-${i}`}
data-idx={i}
onChange={onContentChange}
value={row[i].content} // Error
/>
)
})
})
return retArr;
}, [data])
However, row[i] is empty at that moment. Maybe useRow function in useEffect is executed after factory of useMemo done.
Since there is a callback at useEffect, is there any way to control order of useEffect and useMemo?
Any other opinions are appreciated!

Related

Empty Object on React useEffect

In my project I have the component ExportSearchResultCSV. Inside this component the nested component CSVLink exports a CSV File.
const ExportSearchResultCSV = ({ ...props }) => {
const { results, filters, parseResults, justify = 'justify-end', fileName = "schede_sicurezza" } = props;
const [newResults, setNewResults] = useState();
const [newFilters, setNewFilters] = useState();
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true)
const [headers, setHeaders] = useState([])
const prepareResults = () => {
let newResults = [];
if (results.length > 1) {
results.map(item => {
newResults.push(parseResults(item));
}); return newResults;
}
}
const createData = () => {
let final = [];
newResults && newResults?.map((result, index) => {
let _item = {};
newFilters.forEach(filter => {
_item[filter.filter] = result[filter.filter];
});
final.push(_item);
});
return final;
}
console.log(createData())
const createHeaders = () => {
let headers = [];
newFilters && newFilters.forEach(item => {
headers.push({ label: item.header, key: item.filter })
});
return headers;
}
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters])
return (
<div className={`flex ${justify} h-10`} title={"Esporta come CSV"}>
{results.length > 0 &&
<CSVLink data={createData()}
headers={headers}
filename={fileName}
separator={";"}
onClick={async () => {
await setNewFilters(filters);
await setNewResults(prepareResults());
await setData(createData());
await setHeaders(createHeaders());
}}>
<RoundButton icon={<FaFileCsv size={23} />} onClick={() => { }} />
</CSVLink>}
</div >
)
}
export default ExportSearchResultCSV;
The problem I am facing is the CSV file which is empty. When I log createData() function the result is initially and empty object and then it gets filled with the data. The CSV is properly exported when I edit this component and the page is refreshed. I tried passing createData() instead of data to the onClick event but it didn't fix the problem. Why is createData() returning an empty object first? What am I missing?
You call console.log(createData()) in your functional component upon the very first render. And I assume, upon the very first render, newFilters is not containing anything yet, because you initialize it like so const [newFilters, setNewFilters] = useState();.
That is why your first result of createData() is an empty object(?). When you execute the onClick(), you also call await setNewFilters(filters); which fills newFilters and createData() can work with something.
You might be missunderstanding useEffect(). Passing something to React.useEffect() like you do
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters]) <-- look here
means that useEffect() is only called, when results or filters change. Thus, it gets no executed upon initial render.

Why does my prop not having my adding field?

can someone see why do I have selectionNames:[] empty when I console log ??
In the rest of my code, I'm using only selectionNames, it has the same content of names, but with an extra field selected. Therefore, I've thought using one sate i.e names instead of 2 i.e selectionNames and modifying names directly in my useEffect (to add my selected field but it's not working.
Can someone see where is the issue please ?
export default function Display() {
const [names, setNames] = useState([])
useEffect(() => {
axios.post("")
.then(res => {
console.log(res)
setNames(res.data.names)
})
.catch(err => {
console.log(err)
})
}, []);
const init = (e) => {
return e.map((item) => {
return {..item,types: item.types.map((t) => ({ ...t, selected: true }))
};
});
};
const [selectionNames, setSelectionNames] = useState(init(names));
console.log(selectionNames)
...
const change = (id,item, value) => {setSelectionStandards((s) => s.map((item) => {...} return item;}));
};
return (
<>
{selectionNames.map((item) => (...))}
</>
);
}
Here is my json from my api:
{
"names": [
{
"id": 1,
"Description": "descr",
"types": [
{
"id": 1,
"decription":"descr1",
},
...
]
},
...
]
}
This is what you should do:
import {useState, useEffect} from 'react';
const Display = () => {
// The initial state is empty
const [names, setNames] = useState([]);
// This function will be called when component mounts
const init = async () => {
const {data} = axios.post('');
setNames(data.names);
}
useEffect(() => {
init();
} , [])
// First time, this will print an empty array.
// After the initialization, you will get the actual names array
console.log(names);
}
import {useState, useEffect} from 'react';
const Display = () => {
// The initial state is empty
const [names, setNames] = useState([]);
// This function will be called when component mounts
const init = async () => {
const { data } = await axios.post(''); // put the await before, call axios
setNames(data.names);
}
useEffect(() => {
init();
} , [])
// First time, this will print an empty array.
// After the initialization, you will get the actual names array
console.log(names);
}

how to display my list from store at initialization with easy-peasy?

I want to get a list of churches from the store at initialization but i can't. The log get my initial array and the new one but doesn't display. Log below:
log
here is my model:
const churchModel = {
items: [],
// ACTIONS
setAllChurches: action((state, payload) => {
state.items = payload;
}),
getInitialChurches: thunk(async (actions) => {
const { data } = await axios.post(
'http://localhost:3000/api/geo/closeto?latlong=2.3522219 48.856614&distance=10000'
);
let array = [];
const resData = data.map(async (index) => {
const res = await axios.get(`http://localhost:3000/api/institutions/all?idInstitution=${index.idInstitution}`);
array.push(res.data[0]);
});
actions.setAllChurches(array);
})
}
and my component:
const ChurchList = () => {
const classes = useStyles();
const setInitialChurches = useStoreActions(action => action.churches.getInitialChurches);
const churches = useStoreState(state => state.churches.items);
const [activeItem, setActiveItem] = React.useState(null);
useEffect(() => {
setInitialChurches()
}, []);
return (
<div className={classes.root} style={{marginTop: '20px',}}>
{ churches.map( (church) => (
<ChurchItem
key={ church.idInstitution }
church={ church }
setActiveItem={setActiveItem}
activeItem={activeItem}
/>)
), console.log(churches)}
</div>
)
};
export default ChurchList;
I tried a useEffect but nothing true. Could you help me please ?
that is not a good location to put console.log in, either put it outside the component render, inside the map or on a useEffect.
You can achieve it by using useEffect and passing churches on the array.
useEffect(() => {
// this will log everytime churches changes / initialized churches
console.log(churches);
}, [churches]);

Not sure how to render results of useEffect in browser

Following is the data useEffect is returning in console log:
{
"sql": {
"external": false,
"sql": [
"SELECT\n date_trunc('day', (\"line_items\".created_at::timestamptz AT TIME ZONE 'UTC')) \"line_items__created_at_day\", count(\"line_items\".id) \"line_items__count\"\n FROM\n public.line_items AS \"line_items\"\n GROUP BY 1 ORDER BY 1 ASC LIMIT 10000",
[]
],
"timeDimensionAlias": "line_items__created_at_day",
"timeDimensionField": "LineItems.createdAt",
"order": {
"LineItems.createdAt": "asc"
}
I want to be able to render the above in my react app.
const ChartRenderer = ({ vizState }) => {
let ur = encodeURIComponent(JSON.stringify(vizState.query));
let u = "http://localhost:4000/cubejs-api/v1/sql?query=" + ur;
console.log(u)
useEffect(() => {
if(u !=="http://localhost:4000/cubejs-api/v1/sql?query=undefined") {
fetch(u)
.then(response => (response.json()))
.then(data => console.log(JSON.stringify(data, null, 4)))
}},[u]);
const { query, chartType, pivotConfig } = vizState;
const component = TypeToMemoChartComponent[chartType];
const renderProps = useCubeQuery(query);
return component && renderChart(component)({ ...renderProps, pivotConfig })
};
You would have to use a state variable to persist data and update it whenever the API returns some data. In a functional component, you can use the useState hook for this purpose.
const ChartRenderer = ({ vizState }) => {
// useState takes in an initial state value, you can keep it {} or null as per your use-case.
const [response, setResponse] = useState({});
let ur = encodeURIComponent(JSON.stringify(vizState.query));
let u = "http://localhost:4000/cubejs-api/v1/sql?query=" + ur;
console.log(u)
useEffect(() => {
if(u !=="http://localhost:4000/cubejs-api/v1/sql?query=undefined") {
fetch(u)
.then(response => (response.json()))
.then(data => {
// You can modify the data here before setting it to state.
setResponse(data);
})
}},[u]);
// Use 'response' here or pass it to renderChart
const { query, chartType, pivotConfig } = vizState;
const component = TypeToMemoChartComponent[chartType];
const renderProps = useCubeQuery(query);
return component && renderChart(component)({ ...renderProps, pivotConfig })
};
You can read more about useState in the official documentation here.

How to handle an unchanging array in useEffect dependency array?

I have a component like this. What I want is for the useEffect function to run anytime myBoolean changes.
I could accomplish this by setting the dependency array to [myBoolean]. But then I get a warning that I'm violating the exhaustive-deps rule, because I reference myArray inside the function. I don't want to violate that rule, so I set the dependency array to [myBoolean, myArray].
But then I get an infinite loop. What's happening is the useEffect is triggered every time myArray changes, which is every time, because it turns out myArray comes from redux and is regenerated on every re-render. And even if the elements of the array are the same as they were before, React compares the array to its previous version using ===, and it's not the same object, so it's not equal.
So what's the right way to do this? How can I run my code only when myBoolean changes, without violating the exhaustive-deps rule?
I have seen this, but I'm still not sure what the solution in this situation is.
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
useEffect(() => {
if(myBoolean) {
setMyString(myArray[0]);
}
}, [myBoolean, myArray]
}
Solution 1
If you always need the 1st item, extract it from the array, and use it as the dependency:
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const item = myArray[0];
useEffect(() => {
if(myBoolean) {
setMyString(item);
}
}, [myBoolean, item]);
}
Solution 2
If you don't want to react to myArray changes, set it as a ref with useRef():
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const arr = useRef(myArray);
useEffect(() => { arr.current = myArray; }, [myArray]);
useEffect(() => {
if(myBoolean) {
setMyString(arr.current);
}
}, [myBoolean]);
}
Note: redux shouldn't generate a new array, every time the state is updated, unless the array or it's items actually change. If a selector generates the array, read about memoized selectors (reselect is a good library for that).
I have an idea about to save previous props. And then we will implement function compare previous props later. Compared value will be used to decide to handle function change in useEffect with no dependency.
It will take up more computation and memory. Just an idea.
Here is my example:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function arrayEquals(a, b) {
return Array.isArray(a) &&
Array.isArray(b) &&
a.length === b.length &&
a.every((val, index) => val === b[index]);
}
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const previousArray = usePrevious(myArray);
const previousBoolean = usePrevious(myBoolean);
const handleEffect = () => {
console.log('child-useEffect-call-with custom compare');
if(myBoolean) {
setMyString(myArray[0]);
}
};
//handle effect in custom solve
useEffect(() => {
//check change here
const isEqual = arrayEquals(myArray, previousArray) && previousBoolean === myBoolean;
if (!isEqual)
handleEffect();
});
useEffect(() => {
console.log('child-useEffect-call with sallow compare');
if(myBoolean) {
setMyString(myArray[0]);
}
}, [myBoolean, myArray]);
return myString;
}
const Any = () => {
const [array, setArray] = useState(['1','2','3']);
console.log('parent-render');
//array is always changed
// useEffect(() => {
// setInterval(() => {
// setArray(['1','2', Math.random()]);
// }, 2000);
// }, []);
//be fine. ref is not changed.
// useEffect(() => {
// setInterval(() => {
// setArray(array);
// }, 2000);
// }, []);
//changed ref but value in array are not changed -> handle this case
useEffect(() => {
setInterval(() => {
setArray(['1','2', '3']);
}, 2000);
}, []);
return <div> <MyComponent myBoolean={true} myArray={array}/> </div>;
}

Categories

Resources