How to set a property as an array i initial state? - immutable.js

In my reducer I set initial state by:
const initialState = fromJS({
results: [],
});
However if I try to print results by
initialState.get('results')
I get an immutable Map.
On the other hand if in my reducer (listening to an action) I set the array via
...
case LOAD_SUCCESS:
return state
.set('results', []);
...
the array will be an actual (non-Immutable) array after executing:
state.get('results')
(e.g. in a selector defined via reselect)
Why?

From the fromJS docs:
Deeply converts plain JS objects and arrays to Immutable Maps and Lists.
That said, fromJS({ results: [] }) is equal to Map({ results: List([])})
as soon as you call state.set('results', []), you replace List([]) with plain array [].
This is a classical trap for new starters.
The way I see it is you have to choose to either always use List there, or plain array.
List way:
Initialize: const state = fromJS({results: []})
Reduce: return state.update("results", list => list.clear())
Array way #1:
Initialize: const state = fromJS({results: null})
INIT reduce (dispatched once, to init state) return state.set("results", [])
Reduce: return state.set("results", [1, 2, 3])
Array way #2:
Initialize: const state = Map({results: []})
Reduce: return state.set("results", [1, 2, 3])
I'd recommend to always use Array way #2. It adds more code, as you have to control List / Map for each state's field, but it guarantees you that you get exactly what you want.
In the end it comes something like this:
const initialState = Map({
results: [],
some_field_as_list: List([]),
some_field_as_map: Map([]),
... // etc
});

fromJS({ results: [] }) is equal to Map({ results: List([])}),
Using state.set('results', []) you replace List([]) with plain []
I found this helpful in this case:
Initialize: const state = fromJS({results: []})
Reduce: return state.set('results', List([1,2,3]));
It was better than using plain array because later you can use setIn function editing the same state variable:
return state.setIn(['results',state.get("results").size], nextElement);
or
return state.setIn(['results',-1], lastElement);

Related

Displaying Node MySQL Results in React Using State

My node.js MySQL query returns a single row wrapped in [RowPacketData] which I can normally access the ID field using results[0].ID.
However, when I store the result in React state (using hooks) it does not work. I can access the result object, but not fields within it.
function MyReactComponent() {
const [dbEntry, setDbEntry] = useState();
useEffect(() => {
const fetchData = async () => {
const result = await queryFunc(`SELECT * FROM table LIMIT 1`);
console.log(result[0]); // <-- Works (shows [RowDataPacket] object)
console.log(result[0].ID); // <-- Works (shows ID)
setDbEntry(result);
};
fetchData();
}, []);
console.log(dbEntry[0]); // <-- Works (shows [RowDataPacket] object)
console.log(dbEntry[0].ID); // <-- TypeError: Cannot read property '0' of undefined
return (
<p>
{dbEntry[0].ID} // <-- How do I render here?
</p>
)
}
What's going on here? I have a feeling React is coercing the result object somehow, but I can't figure it out...
When you need to display data that comes from an async font(API calls for example), it's possible (actually almost certain) that it won't be available by the time the first render occurs, to solve that there is actually a few things you could do:
Placeholder state
You could have a model of what the data will look like described as your initial state, so properties won't be undefined anymore:
const [state, setState] = useState({
data:[
{name: ''}
]
})
Assuming that your data will have this format accessing state.data[0].name won't throw an error. This could be useful in some cases but I personally don't like the approach.
Conditional Render
At each render you should check for a condition and only if satisfied render the piece of code:
return(
<>
<div>Title</div>
{Boolean(state.data.length) && <div>{state.data[0].name}</div>}
</>
)
Suspense
That one is brand new, if you have a component tha't need to perform side effects before render it's content, you should have a fallback content to be displayed while the async action is being perform.
<Suspense fallback={<span>Loading</span>}>
<MYAsyncComponent />
</Suspense>

Sequelize raw queries TextRow and getting data out of it

Given this query here,
let output = [];
const sql = `select * from coredb.account LIMIT ${offset},${limit}`;
let data = await sequelize.query(sql, null, {raw: true, type: sequelize.QueryTypes.SELECT});
data.forEach((item) => {
console.log(item['id'], item.id); // <-- output says "undefined, undefined"
});
the data variable is indeed hydrated with the right row data when using console.log to inspect it.
But, when I try to access the individual properties, they only ever come back as undefined. This TextRow object that Sequelize seems to return the result in doesn't seem to want to let me access then explicit rows.
Just curious what i'm missing here, am I missing an option?
I agree, Sequalize raw queries are not intuitive. You don't need the null or raw: true flag. Something like this should work:
let data = await sequelize.query(sql, {type: sequelize.QueryTypes.SELECT});
When I tried this, "data" was an array of two objects, each being the query result. So, the properties can be accessed by using index [0].... e.g.
data[0].forEach((item) => {
console.log(item['id'], item.id); // <-- output says "undefined, undefined"
});
Not yet sure WHY this occurs!
EDIT - it's because .query() should have only two arguments. Changing the call to: sequelize.query(sql, {raw: true, type: sequelize.QueryTypes.SELECT}) resulted in data being a single array (as expected).
Finally I was able to find the solution for it.
You just need to make a new array and push data into it by finding bases on key name like this:
suppose we have data in students object:
let finalArray = new Array();
for (var k in students ) {
finalArray.push(students[k])
}
console.log(finalArray) // Normal JSON array object :)
m.sequelize.query(sql, {
model,
mapToModel: true
})
.then(model => res.status(200).send(model))
.catch(error => res.status(400).send(error.toString())
})

Unable to add new key-value pair dynamically to a STATE JSON Array in React App

I am trying to add a new key-value pair to the already loaded JSON Array. I am adding the new key-value pair to customize the header column cells in react bootstrap table but getting the below errors. Can any one please help?
'Columns' in the below state is where I wanted to add new key-value pair
state = {
data: MYResult.Products || [],
actualData: MYResult.Products || [],
columns: MYResult.ParametricList_Attributes || [],
isCompareClicked: false,
isDisabled: true,
selected: []
};
This is how I am adding the key-value pair -
componentDidMount(){
checkbox = (column, colIndex) => {
return (
<h5>{ column.text }<checkbox/></h5>
);
}
console.log(this.state.columns) ;
newColumn = this.state.columns.map((column) => {
return {...column, headerFormatter: checkbox};
});
this.setState({columns: newColumn });
}
Full code here - https://codesandbox.io/s/o1r988qkz Please uncomment the componentDidMount() to see the issue
Firstly, there's a typo in dcolumn and column.
And regarding the not defined error, you need to define it using const. Use like:
const checkbox = (column, colIndex) => {
return (
<h5>{column.text}<checkbox /></h5>
);
}
JavaScript variables need to be declared when they are used. Public class syntax can not be used everywhere. The error you're getting is self-evident - 'checkbox is not defined'.
Refer this on how to use it: https://tylermcginnis.com/javascript-private-and-public-class-fields/
I simply declared the undeclared variables in your example and the code worked.

Create Country List from JSON

I am trying to create a combo box with a list of countries in SAP UI5.
I have created a combo box and have created dynamic list of some countries, but to create more than 100 countries, the only easy way is to create a JSON file of countries and then populate in Controller.js.
I tried to create a JSON file but I am unsure whether I have to store it under model folder or root.
What changes do I have to make in my XML view and controller, and where should I attach countries.json file?
You are looking at something called as "Aggregation Binding" Aggregation Binding in XML views
Here is an example to refer to which explains
How to create a model using data from json file
How to Bind model data to the XML view control(you have to bind comboBox instead of table)
How to bind json data model to an XML view
Let me know if this helps.
Maybe you don't need to create the countries.json file at all :)
As UI5 leverages Common Locale Data Repository (CLDR) internally and provides the data via sap.ui.core.LocaleDataAPI, which includes language names, country names, currency names, singular/plural modifications, and more..
A list of supported regions for the locale data are stored in a JSON format here. In one of those files, if you look at the property "territories", you'll see that the country names are listed among them. You can filter every irrelevant territory out that is not considered a country, and then bind the rest in the items aggregation of the combo box.
Demo
sap.ui.getCore().attachInit(() => sap.ui.require([
"sap/ui/core/Locale",
"sap/ui/core/LocaleData",
"sap/ui/model/json/JSONModel",
"sap/ui/core/mvc/XMLView",
], function(Locale, LocaleData, JSONModel, XMLView) {
"use strict";
XMLView.create({
definition: `<mvc:View xmlns:mvc="sap.ui.core" xmlns="sap.m"
height="100%"
displayBlock="true">
<ComboBox class="sapUiTinyMargin"
width="15rem"
placeholder="Select a country.."
filterSecondaryValues="true"
showSecondaryValues="true"
items="{
path: '/',
templateShareable: false,
key: 'code',
sorter: { path: 'name' }
}">
<core:ListItem xmlns:core="sap.ui.core"
key="{code}"
text="{name}"
additionalText="{code}" />
</ComboBox>
</mvc:View>`,
models: createCountryModel(getCountries()),
}).then(view => view.placeAt("content"));
function createCountryModel(countries, sizeLimit = 300) {
const model = new JSONModel(countries);
model.setSizeLimit(sizeLimit);
model.setDefaultBindingMode("OneWay");
return model;
}
function getCountries() {
const territories = getTerritories();
return extractCountriesFrom(territories, byCustomCheck());
}
function getTerritories(localeId) {
const currentConfig = sap.ui.getCore().getConfiguration();
const locale = localeId ? new Locale(localeId) : currentConfig.getLocale();
const localeData = new LocaleData(locale);
return localeData.getTerritories(); // includes country names
}
function extractCountriesFrom(territories, customCheck = () => true) {
const isValidCountry = createCountryCheck(customCheck);
const toObject = code => Object.freeze({
code: code,
name: territories[code],
});
const countryObjects = Object.keys(territories)
.filter(isValidCountry)
.map(toObject);
return Object.freeze(countryObjects);
}
function createCountryCheck(customCheck, obviouslyNotCountries = [
"EU", // "European Union"
"EZ", // "Eurozone"
"UN", // "United Nations"
"ZZ", // "Unknown Region"
]) {
return territoryCode => territoryCode.length == 2
&& !obviouslyNotCountries.includes(territoryCode)
&& customCheck(territoryCode);
}
function byCustomCheck() { // returns a function that returns boolean
// E.g.: list of sanctioned countries you want to exclude
const list = [
"AF",
"KP",
"IR",
// ...
];
return countryCode => !list.includes(countryCode);
}
}));
<script id="sap-ui-bootstrap" src="https://ui5.sap.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.ui.core, sap.m"
data-sap-ui-theme="sap_fiori_3"
data-sap-ui-async="true"
data-sap-ui-compatversion="edge"
data-sap-ui-xx-waitfortheme="init"
></script>
<body id="content" class="sapUiBody sapUiSizeCompact"></body>
As you can see in the example, the ComboBox is successfully populated with the countries. When a new LocaleData instance is created, a request is sent immediately (currently via sync XHR) to get the data which are translated in the language that UI5 detects from the client settings. If no language could be detected, the en.json file will be retrieved.src
The above approach has the following advantages:
No need to create and maintain a separate "country" list. ✔️
Multilingual support ✔️
Reusability ✔️ - When UI5 tries to fetch the same locale data file, which is the case when e.g. a Calendar is used, the browser can serve the file quickly from the cache since the same file was already fetched before.
Note
When creating a JSONModel to store more than 100 country names, keep in mind to increase the size limit as well. The current default limit is 100.

accessing properties of nested objects with Lodash

Lodash's documentation for map() contains an example like this:
var users = [{'user': 'barney'}, {'user': 'fred'}];
_.map(users, 'user'); // => ['barney', 'fred']
I have data similar to that, but nested. Theirs is an array of objects, but I have an array of objects, each of which contains an array of objects, too. Expanding on the Lodash example, my data is like this:
var users = [
{'mapping': [{'user': 'barney'}, {'user': 'fred'}]},
{'mapping': [{'user': 'sherlock'}, {'user': 'watson'}]},
];
I'd like to get back all four of those names. (An array of arrays is acceptable.)
I tried a number of ways to do this. You can see my attempts at: https://runkit.com/lsloan0000/lodash-map-nested
Eventually, I found this solution:
// I didn't think it would take this much code
_.map(users, function (value, key, collection) {
return _.map(value.mapping, 'user');
});
I thought Lodash has so many features that I wouldn't need to use a callback function.
Is there a simpler way to accomplish this?
I know I can probably skip the key and collection arguments. Eventually, I plan to have this code return the objects whose mapping contains two specific names. (All objects with mappings for "fred" and "barney" together.) So I've left those arguments there, because I think I will need them for that purpose.
Use _.flatMap() to flatten the nested array into a single array, and then map it to user values:
var users = [
{'mapping': [{'user': 'barney'}, {'user': 'fred'}]},
{'mapping': [{'user': 'sherlock'}, {'user': 'watson'}]},
];
var result = _(users)
.flatMap('mapping')
.map('user')
.value();
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
And without a lodash chain:
var users = [
{'mapping': [{'user': 'barney'}, {'user': 'fred'}]},
{'mapping': [{'user': 'sherlock'}, {'user': 'watson'}]},
];
var result = _.map(_.flatMap(users, 'mapping'), 'user');
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>