How to create dynamic route if there is no unique id from backend? - react-router

When using public API which provide data with unique id I can simple use this id to create dynamic route e.g. ${book.id}. But some API do not provide unique id, so how can I replace my ${book.id} in this case?
Code example that works when id provided:
<Routes>
<Route path="/book" element={<BooksView />} />
<Route path="/book/:bookId" element={<BookDetailsView />} />
</Routes>
return(
{books && (
<ul>
{books.map((book) => (
<li key={book.id}>
<Link to={`/books/${book.id}`}>{book.title}</Link>
</li>
))}
</ul>
)})
I tried to generate unique id with uuid library but it didn't work, maybe because it creates string type data whereas number type needed

I've had to work with some APIs that returned data either without ids or data partially with ids.
If you need a valid book.id property to interface with external (to the app) 3rd-party APIs then there's not much you can do here since you can't really make up an id value that would work in this situation.
So long as the book.id property is only used locally to navigate to and display fetched content then the way I've solved this issue is to process the data after it's been fetched prior to saving it into any local state, generating and adding ids when necessary. Here's an example workflow.
import { v4 as uuidV4 } from 'uuid';
...
const fetchBooks = async () => {
const response = await fetch(".......");
const bookData = await response.json();
const processedBookData = bookData.map(book => !!book.id
? book
: { ...book, id: uuidV4() } // <-- generate id if missing
);
/* save processedBookData into books state */
};
BooksView - There should now be a defined id property for each book object that is sufficiently guaranteed to be unique.
return(
{books && (
<ul>
{books.map((book) => (
<li key={book.id}>
<Link to={`/books/${book.id}`}>{book.title}</Link>
</li>
))}
</ul>
)}
);
It shouldn't matter if the book.id property is a string or number-like value as all route path params will be a string type when accessing within a component via the useParams hook. If you have a mix of "types" then the suggestion here is to use a type-safe comparison by converting all id values to strings for comparison.
BookDetailsView
const BookDetailsView = () => {
const { bookId } = useParams();
const book = books.find(book => String(book.id) === bookId);
if (!book) {
// book not found
}
// book found
...
};
If you need to generate ids and interact with external APIs then you could set a flag in the books data that indicates if the id value is real or not, and use this to conditionally allow any external interactions.
const processedBookData = bookData.map(book => !!book.id
? book
: {
...book,
id: uuidV4(),
internalId: true
}
);
if (!book.internalId) {
// id is legitimate, use for external calls/effects
}

Related

How to initialize checkbox based on dynamic value in React

I am trying to initialize a checkbox to either checked or unchecked based on a value in local storage (chrome extension). I was trying to trigger a function on load that checks the a value in local storage and returns a Boolean value. Since there are multiple checkboxes I need to pass the element id to the function as well.
Here is a semi-pseudo code version of what I was thinking :
const getChecked = (el: any): boolean => {
val = el.id from local storage;
return val;
}
const App = () => (
<div className="App">
<input
type="checkbox"
id="checkboxID"
defaultChecked={getChecked}
/>
</div>
);
But something like this doesn't work because defaultChecked={getChecked} gives me the error " Type '(el: any) => boolean' is not assignable to type 'boolean | undefined'."
Any ideas on what to do?
Thanks.
Please try using checked parameter, instead of defaultChecked.
To get the value from local storage I would suggest you to use a function like this:
const getChecked = (el:any) => {
return JSON.parse(localStorage.getItem("el.id"))
};
Make sure that el.id value is true or false in local storage.

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>

Is there a way to fetch data in database from a Ruby-on-Rails models

I have a rails app running alongside with a rails API, there is a constant value for DAYS_LIMIT in config/initializers/constants.rb
DAYS_LIMIT = 40
DEFAULT_PRICE = 1.29
but now in the app i added an input field so that the user decide his DAYS_LIMIT.
So i want to fetch that value from the database from inside the API models.
I have placed breakpoints and can see that inside the API controller, the data is transfered from the app but not to the models.
edited as a question requested , it's a React-on-Rails app , here is the code where the new input field is save to the database (i have removed the other fields so the question look shorter)
export const saveChannel = (files) => {
return async (dispatch, getState) => {
const { channel } = getState();
const {rss_podcast_days} = channel;
const { image } = files;
const save = id ? updateChannel : createChannel;
const sub_required = subscription_required !== undefined ? subscription_required : false;
const formData = new FormData();
formData.append('channel[rss_podcast_days]', rss_podcast_days || '');
if (Object.keys(image).length) {
formData.append('channel[image]', image);
}
const channelId = await dispatch(save(formData, id));
dispatch(fetchChannel(id));
return id;
};
};
from the app controller
podcast_list = RestClient.get("#{ENV['URL_API']}/api/#{#channel.id.as_json}/podcast/list")
#podcasts = JSON.parse(podcast_list.body)
#podcasts = #podcasts.sort.reverse.to_h
this is from the API controller witch the data is transfered from the app
def index
podcasts = #channel.podcasts.published.list(params[:page], params[:items_per_page], params[:ordered_in])
render json: Podcasts::Normalizer.normalize(podcasts, #channel.station.default_podcast_price)
end
and here from the API model that i want to fetch data instead of the constants.
scope :by_days_limit, -> {with_tags.more_recent_than(Date.today - DAYS_LIMIT.days).ordered}
it should take today date minus the value (DAYS_LIMIT) from user input, but for now i get undefined local variable or method if i try to fetch directly
Bro if your class has constant like DAYS_LIMIT you can access it using that class itself for example,
class Demo
DAYS_LIMIT = 5
end
you can access that constant by Demo.DAYS_LIMIT in controller or else wherever you need it.
good luck!
ok , so i finally got it, i don't know if i should delete this thread or just tell how i did it. If it's inapropriate just tell me and i will delete this entire thread.
So here is how i did it, in the API controller i had to add my fetch so that the arguments (list) knows what i am talking about. #channel.days_limit
def index
podcasts = #channel.podcasts.published.list(params[:page], params[:items_per_page], params[:ordered_in], #channel.days_limit)
render json: Podcasts::Normalizer.normalize(podcasts, #channel.station.default_podcast_price)
end
then in the def list of the models, i added days_limit has argument
def list(page = nil, nb_items_per_page = 40, ordered_in = 'desc', days_limit)
ordered_in = ordered_in.in?(['asc', 'desc']) ? ordered_in : 'desc'
page.blank? ? by_days_limit(days_limit) : by_page(page, nb_items_per_page, ordered_in)
end
and finally in the scope of the models, i pass in the new argument
scope :by_days_limit, -> (days_limit) {with_tags.more_recent_than(Date.today - days_limit.days).ordered}
Now the user input from the app is passing to the models via the controller.

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.

Combine two Json files exported from wordpress

I have two Json files that I exported from wordpress that have corresponding ID's I want to combine them into one Json file so I can bring it into website I am building with Gatsby JS. One of the files is the posts.json and the other is postsMeta.json. The post_id in postsMeta corresponds with the ID in Posts
How would I best go about merging the two? Can I run some sort of for loop in js and how would I so? I am on windows is there a json explorer of some sorts that could help me do this.
lastly I would also like to trim out some of the unnecasry fiels such as post_parent in the posts json and something like the meta_key in the postsMeta json.
Ok hopefully this is clear enough, thanks in advance.
Here is an example of the first object corresponding pairs in the two files
posts.json
{"ID":"19","post_author":"2","post_date":"2010-12-31 23:02:04","post_date_gmt":"2010-12-31 23:02:04","post_content":"Harry Potter was not available for the first sitting of the Halloween Picture. I hope everyone had a safe and fun Halloween. Tomorrow is picture retake day, please send back your previous prints if you want retakes. It is also hot lunch. See You tomorrow!","post_title":"Happy Halloween","post_excerpt":"","post_status":"publish","comment_status":"open","ping_status":"open","post_password":"","post_name":"happy-halloween","to_ping":"","pinged":"","post_modified":"2011-01-03 05:26:11","post_modified_gmt":"2011-01-03 05:26:11","post_content_filtered":"","post_parent":"0","guid":"http:\/\/localhost\/mrskitson.ca_wordpress\/?p=19","menu_order":"0","post_type":"post","post_mime_type":"","comment_count":"1"},
postsMeta.json
{"meta_id":"27","post_id":"19","meta_key":"large_preview","meta_value":"http:\/\/www.mrskitson.ca\/wp-content\/uploads\/2010\/12\/halloween.jpg"},
Update:
this is an attempt to solve this problem with the current answer, you can edit the code there.
How would I best go about merging the two?
Is it mandatory for you combine the two JSON files/data?
Because you could just require or load the JSON data from within your script (or even put them in the HTML) and then to get the meta value of a specific meta field/key, this function could do that:
// `single` has no effect if `meta_key` is empty.
function getPostMeta( post_id, meta_key, single ) {
let id = String( post_id ),
pm = [];
postsMeta.map( m => {
let a = ( ! meta_key ) ||
( meta_key === m.meta_key );
if ( a && id === m.post_id ) {
pm.push( m );
}
});
let meta = {},
mk = {};
pm.map( m => {
let k = m.meta_key, v;
if ( undefined === meta[ k ] ) {
meta[ k ] = m.meta_value;
} else {
v = meta[ k ];
if ( undefined === mk[ k ] ) {
meta[ k ] = [ v ];
mk[ k ] = 1;
}
meta[ k ].push( m.meta_value );
m[ k ]++;
}
});
pm = null;
mk = meta_key ? mk[ meta_key ] : null;
if ( mk ) {
return single ?
meta[ meta_key ][0] : // Returns a single meta value.
meta[ meta_key ]; // Returns all the meta values.
}
return meta_key ?
meta[ meta_key ] : // Returns the value of the `meta_key`.
meta; // Or returns all the post's meta data.
}
The data I used for testing: (take note of the postsMeta in the above/getPostMeta() function)
// Array of `post` objects.
const posts = [{"ID":"19","post_author":"2","post_date":"2010-12-31 23:02:04","post_date_gmt":"2010-12-31 23:02:04","post_content":"Harry Potter was not available for the first sitting of the Halloween Picture. I hope everyone had a safe and fun Halloween. Tomorrow is picture retake day, please send back your previous prints if you want retakes. It is also hot lunch. See You tomorrow!","post_title":"Happy Halloween","post_excerpt":"","post_status":"publish","comment_status":"open","ping_status":"open","post_password":"","post_name":"happy-halloween","to_ping":"","pinged":"","post_modified":"2011-01-03 05:26:11","post_modified_gmt":"2011-01-03 05:26:11","post_content_filtered":"","post_parent":"0","guid":"http:\/\/localhost\/mrskitson.ca_wordpress\/?p=19","menu_order":"0","post_type":"post","post_mime_type":"","comment_count":"1"}];
// Array of `meta` objects.
const postsMeta = [{"meta_id":"27","post_id":"19","meta_key":"large_preview","meta_value":"http:\/\/www.mrskitson.ca\/wp-content\/uploads\/2010\/12\/halloween.jpg"},{"meta_id":"28","post_id":"19","meta_key":"many_values","meta_value":"http:\/\/facebook.com"},{"meta_id":"29","post_id":"19","meta_key":"many_values","meta_value":"http:\/\/twitter.com"},{"meta_id":"30","post_id":"19","meta_key":"many_values","meta_value":"http:\/\/linkedin.com"}];
Examples: (see this Fiddle for demo)
// In these examples, we are retrieving the meta value for the post #19 (i.e. ID is 19).
// Retrieve a single value.
// Returns mixed; string, number, etc.
let url = getPostMeta( 19, 'large_preview', true );
console.log( url );
// Retrieve all meta values.
// Always returns an array of values.
let ms = getPostMeta( 19, 'many_values' );
console.log( ms, ms[0] );
// Retrieve all meta data.
// Always returns an object with meta_key => meta_value pairs. I.e. { key => value, ... }
let ma = getPostMeta( 19 );
console.log( ma, ma.large_preview, ma.many_values[0] );
But if you really must combine the JSON data, you can do: (again, see demo on the same Fiddle)
// Here we modify the original `posts` object.
posts.map( p => {
// Add all the post's meta data.
p.meta = getPostMeta( p.ID );
// Delete items you don't want..
delete p.post_parent;
delete p.menu_order;
// delete ...;
});
console.log( JSON.stringify( posts[0].meta ) ); // posts[0].meta = object
console.log( posts[0].post_parent, posts[0].menu_order ); // both are undefined
And then if you want to copy-paste the new/merged JSON data:
JSON.stringify( posts );
But if you actually just want to do something with the post's meta, you can loop through the posts object and do the thing; e.g.:
// Here the original `posts` object is not modified, and that we don't
// (though you can) repeatedly call `getPostMeta()` for the same post.
posts.map( p => {
// Get all the post's meta data.
let meta = getPostMeta( p.ID );
// Do something with `meta`.
console.log( meta.large_preview );
});
console.log( JSON.stringify( posts[0].meta ) ); // posts[0].meta = undefined
console.log( posts[0].post_parent, posts[0].menu_order ); // both still defined
// posts[0].meta wouldn't be undefined if of course posts[0] had a `meta` item,
// which was set in/via WordPress...
If you can do this in js, there's a pretty easy approach using Array#map. If you simplify your question, you're really asking how to add this meta data under each entry in posts, and get only the fields you want.
I'm assuming the posts.json is actually an array (e.g. [{"ID":"19"....).
// Load these server-side, fetch them remotely, copy-paste, etc.
// I'll require them here for simplicity
const posts = require('./posts.json');
const postsMeta = require('./postsMeta.json');
// Build a Map so we can quickly look up the metas by post_id
// Extract what we need by destructuring the args
const metaByPost = postsMeta.reduce((a, {
post_id: id,
meta_value: value,
}) => a.set(id, {
value,
/* anything else you want in here */,
}), new Map());
const mergedPosts = posts.map(post => ({
// Spread in the post
...post,
// Spread in the meta content
...metaByPost.get(post.ID),
// Undefine the props we don't want
post_parent: undefined,
}));
I don't love manually setting stuff to undefined -- I think it's nicer to explicitly say what props you're going to include, instead of loading everything and undefining certain props.
Try this snippet directly in the Chrome DevTools console:
(function(
postsUrl='https://cdn.glitch.com/61300ea6-6cc4-4cb6-a62f-31adc62ea5cc%2Fposts.json?1525386749382',
metaUrl='https://cdn.glitch.com/61300ea6-6cc4-4cb6-a62f-31adc62ea5cc%2Fpostmeta.json?1525386742630'
) {
Promise.all([
fetch(postsUrl).then(r => r.json()),
fetch(metaUrl).then(r => r.json()),
]).then(([postsResponse, metaResponse]) => {
// Inspected the actual JSON response to come up with the data structure
const posts = postsResponse[2].data;
const meta = metaResponse[2].data;
const metaByPostId = meta.reduce((accum, el) => {
accum[el.post_id] = el;
return accum;
}, {});
const transformedPosts = posts.map(post => {
const merged = {
...post,
...(metaByPostId[post.ID] || {}),
};
delete merged.post_parent;
// delete any other fields not wanted in the result
return merged;
});
console.log(transformedPosts);
});
})();
replace URLs accordingly, I used the ones from Glitch example here
as commented, actual data is buried in response[2].data. Use Network tab / Parsed view to see structure
replace console.log with copy, if you want the result copied to clipboard, instead of logged to console
Blunt to the point for your question. We want to:
merge var a = {/*some json*/} into var b = {/*another json*/}
trim fields in var exclusions = ["post_parent","meta_key"]
Merge the JSONS
First, we need to populate a and b.
Your JSONs are parsable into Javascript objects with JSON.parse():
let a = JSON.parse(/*JSON here*/);
let b = JSON.parse(/*JSON here*/);
Because how properties are defined in Javascript, if you define a property again, the second definition will overwrite the first. Your JSONS contain only strings as keys and strings as values, so a shallow copy will suffice. Object.assign() will copy all the properties (field and values) into the first argument and return the final Object. Therefore this will merge a into b, assuming they have different keys, else the values in b will overwrite values in a:
a = Object.assign(a,b);
Otherwise, if they are not disjoint, you have to define some policy on how to join, for example may prioritize one. Below, we keep the values in a instead:
a = Object.assign(b,a);
Since you mentionmed a for loop, the line below does the same as two code lines above and will also allow show you an example on how to write your own custom lambda expression:
Object.keys(a).forEach(k=>b[k]=b[k]?b[k]:a[k]);
Do not wish to touch a and b? Create a third object c.
let c = Object.assign({},a,b)
Lastly (wait until the trim step below is accomplished) JSON.stringify() will convert your merged object back into JSON.
Trim exclusions
Following the third example, we have c merged with all the fields.
First a little hack taken from here:
Object.filter = (obj, predicate) => Object.keys(obj)
.filter( key => predicate(obj[key]))
.reduce( (res, key) => (res[key] = obj[key], res), {} );
Now Objects, just like arrays have a filter prototype, having extended Object prototype. It is not really best practice since this will extend every Object but this function works quite well with respects to the semantics of Javascript and this example serves as an opportunity to keep elegant Javascript styles code:
c = Object.filter(c, key=> !exclusions.includes(key) );
Voit-lá, done.
As for defined Object.filter() it uses Array.filter() and Array.reduce() . Click for reference, for your convenience.