ngrx dealing with nested array in object - javascript

I am learning the redux pattern and using ngrx with angular 2. I am creating a sample blog site which has following shape.
export interface BlogContent {
id: string;
header: string;
tags: string[];
title: string;
actualContent: ActualContent[];
}
and my reducer and actions are as following:
import { ActionReducer, Action } from '#ngrx/store';
import * as _ from 'lodash';
export interface ActualContent {
id: string;
type: string;
data: string;
}
export interface BlogContent {
id: string;
header: string;
tags: string[];
title: string;
actualContent: ActualContent[];
}
export const initialState: BlogContent = {
id: '',
header: '',
tags: [],
title: '',
actualContent: [],
};
export const ADD_OPERATION = 'ADD_OPERATION';
export const REMOVE_OPERATION = 'REMOVE_OPERATION';
export const RESET_OPERATION = 'RESET_OPERATION';
export const ADD_IMAGE_ID = 'ADD_IMAGE_ID';
export const ADD_FULL_BLOG = 'ADD_FULL_BLOG';
export const ADD_BLOG_CONTENT_OPERATION = 'ADD_BLOG_CONTENT_OPERATION';
export const ADD_BLOG_TAG_OPERATION = 'ADD_BLOG_TAG_OPERATION';
export const blogContent: ActionReducer<BlogContent> = (state: BlogContent= initialState, action: Action ) => {
switch (action.type) {
case ADD_OPERATION :
return Object.assign({}, state, action.payload );
case ADD_BLOG_CONTENT_OPERATION :
return Object.assign({}, state, { actualContent: [...state.actualContent, action.payload]});
case ADD_BLOG_TAG_OPERATION :
return Object.assign({}, state, { tags: [...state.tags, action.payload]});
case REMOVE_OPERATION :
return Object.assign({}, state, { actualContent: state.actualContent.filter((blog) => blog.id !== action.payload.id) });
case ADD_IMAGE_ID : {
let index = _.findIndex(state.actualContent, {id: action.payload.id});
console.log(index);
if ( index >= 0 ) {
return Object.assign({}, state, {
actualContent : [
...state.actualContent.slice(0, index),
action.payload,
...state.actualContent.slice(index + 1)
]
});
}
return state;
}
default :
return state;
}
};
and this is working fine but i am not sure if its the right approach or should i somehow separate the ActualContent into its own reducer and actions and then merge them.
Sorry if this post does not belong here and you can guide me where should put this post and i will remove it from here. Thanks in advance.
P.S. I have done some research but couldnt find any article that has complex nested objects so that i can refer. Please add any useful blog links of ngrx or related topic which can help me out.

Instead of having a nested structure
export interface BlogContent {
id: string;
header: string;
tags: string[];
title: string;
actualContent: ActualContent[]; <------ NESTED
}
You should have a normalized state.
For example here you should have something like :
// this should be into your store
export interface BlogContents {
byId: { [key: string]: BlogContent };
allIds: string[];
}
// this is made to type the objects you'll find in the byId
export interface BlogContent {
id: string;
// ...
actualContentIds: string[];
}
// ----------------------------------------------------------
// this should be into your store
export interface ActualContents {
byId: { [key: string]: ActualContent };
allIds: string[];
}
export interface ActualContent {
id: string;
// ...
}
So if you try to populate your store it'd look like that :
const blogContentsState: BlogContents = {
byId: {
blogContentId0: {
id: 'idBlogContent0',
// ...
actualContentIds: ['actualContentId0', 'actualContentId1', 'actualContentId2']
}
},
allIds: ['blogContentId0']
};
const actualContentState: ActualContents = {
byId: {
actualContentId0: {
id: 'actualContentId0',
// ...
},
actualContentId1: {
id: 'actualContentId1',
// ...
},
actualContentId2: {
id: 'actualContentId2',
// ...
}
},
allIds: ['actualContentId0', 'actualContentId1', 'actualContentId2']
};
In your logic or view (for example with Angular), you need your nested structure so you can iterate over your array and thus, you don't want to iterate on a string array of IDs. Instead you'd like actualContent: ActualContent[];.
For that, you create a selector. Every time your store change, your selector will kicks in and generate a new "view" of your raw data.
// assuming that you can blogContentsState and actualContentsState from your store
const getBlogContents = (blogContentsState, actualContentsState) =>
blogContentsState
.allIds
.map(blogContentId => ({
...blogContentsState.byId[blogContentId],
actualContent: blogContentsState
.byId[blogContentId]
.actualContentIds
.map(actualContentId => actualContentsState.byId[actualContentId])
}));
I know it can be a lot to process at the beginning and I invite you to read the official doc about selectors and normalized state
As you're learning ngrx, you might want to take a look into a small project I've made called Pizza-Sync. Code source is on Github. It's a project were I've done something like that to demo :). (You should also definitely install the ReduxDevTools app to see how is the store).
I made a small video focus only on Redux with Pizza-Sync if you're interested : https://youtu.be/I28m9lwp15Y

Related

Why does passing props cause it to be embed inside an object?

Is there a way to pass a prop without embedding it inside the object?
For instance, consider the parent component passing the "partners" data
const partners = [
{
name: "Test1",
},
{
name: "Test2",
},
{
name: "Test3",
},
{
name: "Test4",
},
];
const Partners: NextPage = () => {
return (
<>
<PartnerPanel props={partners} />
</>
);
};
The child component
// incorrect type
type PropsType = [
{
name: string;
}
];
//correct type
type PropsType = { props: { name: string; }[]; }
export const PartnerPanel: FC<PropsType> = (props): JSX.Element => {
return <div>{props.props[0].name}</div>;
};
Why is it that the props are embedded inside another props, for instance, I have to do props.props[0].name to get the value? Instead of props[0].name
The nested props object is something you are doing, not something that happens automatically with props.
You have named a prop - props
<PartnerPanel props={partners} />
This is a confusing name. You should probably rename it to be clearer as partners.
<PartnerPanel partners={partners} />
Then you would access it more logically as
export const PartnerPanel: FC<PropsType> = (props): JSX.Element => {
return <div>{props.partners[0].name}</div>;
};
You will also need to change your type
type PropsType = { partners: { name: string; }[]; }

How to pass data to custom Vue Extension in TipTap?

I'm trying to pass data to a custom vue component that gets rendered inside the tiptap editor. I can pass default properties but assigning reactive values to it doesn't seem to work.
This is the tiptap-node-extension.js file:
import {Node, mergeAttributes} from '#tiptap/core'
import {VueNodeViewRenderer} from '#tiptap/vue-3'
import Component from '#/views/components/vue-component.vue'
export default Node.create({
parseHTML() {
return [{ tag: 'vue-component' }]
},
renderHTML({ HTMLAttributes }) {
return ['vue-component', mergeAttributes(HTMLAttributes)]
},
addNodeView() {
return VueNodeViewRenderer(Component)
},
})
the script setup portion of the editor component:
<script setup>
import {useEditor, EditorContent, BubbleMenu} from '#tiptap/vue-3'
import StarterKit from '#tiptap/starter-kit'
import {Underline} from "#tiptap/extension-underline";
import {TextAlign} from "#tiptap/extension-text-align";
import {Link} from "#tiptap/extension-link";
import VueComponent from '#/js/tiptap-node-extension.js'
const editor = useEditor({
extensions: [
StarterKit,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
Underline,
Link,
VueComponent.extend({
name: 'vueComponent',
group: 'block',
draggable: true,
addAttributes() {
return {
src: {
default: '123',
}
}
},
}
),
],
content: props.modelValue,
onUpdate: ({ editor }) => {
emit('update:modelValue', editor.getHTML())
},
editable: props.locked ? false : store.admin
})
const sendDataToExtension = async (editor, event) => {
// Triggered upon event
...
state.src = '123'
editor.chain().focus().insertContent('<vue-component/>').run()
}
</script>
and the vue component:
<script setup>
import {NodeViewWrapper} from '#tiptap/vue-3'
const props = defineProps({
node: {
type: Object,
required: true
},
updateAttributes: {
type: Function,
required: true,
}
})
</script>
<template>
<node-view-wrapper class="vue-component" data-drag-handle="">
<p>{{ node.attrs.src }}</p>
</node-view-wrapper>
</template>
The default of src gets through but when I try to assign a reactive object (that gets created after mounting the editor component) it ends up being undefined.
This works:
src: {
default: '123'
}
but this doesn't:
...
src: {
default: state.src
}
...
const sendDataToExtension = async (editor, event) => {
// triggered upon event
...
state.src = '123'
editor.chain().focus().insertContent('<vue-component/>').run()
}
How do I send data to the vue component that is created after mounting editor?
Attempt:
editor.chain().focus().insertContent('<vue-component/>', {src: state.src}).run()
First I would say that I would recommend creating a purpose built extension, instead of having the general VueComponent that you have now. If you extend more based on that extension you will have several extension competing for the tag. Move all code that you set in extend to the actual extentions, you can set any tag-name you want.
Now to what I believe is the problem here: insertContent look like this:
insertContent: (value: Content, options?: {
parseOptions?: ParseOptions;
updateSelection?: boolean;
})
Content is declared as
export declare type Content = HTMLContent | JSONContent | JSONContent[] | null;
export declare type HTMLContent = string;
export declare type JSONContent = {
type?: string;
attrs?: Record<string, any>;
content?: JSONContent[];
marks?: {
type: string;
attrs?: Record<string, any>;
[key: string]: any;
}[];
text?: string;
[key: string]: any;
};
In your case you will have to add the src attribute to your html string, however I would recommend using the JSONContent type in your case then:
editor.chain().focus().insertContent({type: "vueComponent", attrs:{src: state.src}}).run()
Here the type is the name that you set of the component.
Hope this makes sense, the documentation on tiptap is kind of good as well https://tiptap.dev/guide/custom-extensions/#attributes
Let me know if you have further issues.

Typescript dynamically infer type from object

I have a JS Object with React components, indexed by ID.
const MODAL_ENTITIES = {
changeEmail: ChangeEmailModal,
changeUsername: ChangeUsernameModal,
};
I would like to have a ModalEntity type which results in this:
type ModalEntity = {
id: 'changeEmail',
props: React.ComponentProps<typeof ChangeEmailModal>
} | {
id: 'changeUsername',
props: React.ComponentProps<typeof ChangeUsernameModal>
};
My problem is, I want the type to be dynamically generated from the MODAL_ENTITIES object, since I want the process of adding a modal to be as effortlessly as possible.
Is there a way to define this type dynamically? I could do this but I want to avoid generics, I would like T to be inferred:
export type ModalEntity<T extends keyof typeof MODAL_ENTITIES> = {
id: T;
props: React.ComponentProps<typeof MODAL_ENTITIES[T]>;
};
I made a mockup. The idea is to get generic T out of your ModalEntity type so that it can be used easily when you add a new modal.
Placeholders for your modals, assuming that each modal has different props:
import React from 'react';
const ChangeEmailModal: React.FC<{ id: string; name: string; email: string }> = ({ id, ...props }) => {
return (
<div id={id}>
{props.name} {props.email}
</div>
);
};
const ChangeUsernameModal: React.FC<{ id: string; otherName: string; username: string }> = ({ id, ...props }) => {
return (
<div id={id}>
{props.otherName} {props.username}
</div>
);
};
const MODAL_ENTITIES = {
changeEmail: ChangeEmailModal,
changeUsername: ChangeUsernameModal
};
Then we get the keys from your MODAL_ENTITIES in a dynamic way:
export type ModalEntities = typeof MODAL_ENTITIES;
// this gets all the keys in type ModalEntities
type StringKeys<T> = {
[k in keyof T]: k;
}[keyof T];
type ModalEntitiesKeys = StringKeys<ModalEntities>;
Finally:
export type ModalEntity = {
[K in ModalEntitiesKeys]: {
id: K;
props: React.ComponentProps<typeof MODAL_ENTITIES[K]>;
};
}[ModalEntitiesKeys];
The ModalEntity type will look like this and it's no longer generic. the type of props fields will be inferred dynamically as you requested regardless of different modal props.
type ModalEntity = {
id: "changeEmail";
props: {
id: string;
name: string;
email: string;
} & {
children?: React.ReactNode;
};
} | {
id: "changeUsername";
props: {
id: string;
otherName: string;
username: string;
} & {
children?: React.ReactNode;
};
}
You can elaborate more on this idea.

Argument of type is not assignable to parameter of type 'SetStateAction<never[]>'. in React

I am new to React hooks and trying to learn it. I have a simple get api which is served in heroku server. I have following two components ContactCards and ContactCard in which I have provided the api in ContactCards.
Here I have thumbnai and title as static data where as data1 and data2 are data coming from the api.
My ContactCard
import React, { Component } from "react";
interface IContactCardProps {
thumbnail?: string;
title?: string;
data1?: string;
data2?: string;
}
interface IContactCardState {}
class ContactCard extends Component<IContactCardProps, IContactCardState> {
render() {
return (
<div className="contact-card">
<div className="container">
<div className="thumbnail">
<img src={this.props.thumbnail} alt={this.props.title} />
</div>
<div className="title">{this.props.title}</div>
<div className="data">{this.props.data1}</div>
<div className="data">{this.props.data2}</div>
</div>
</div>
);
}
}
export default ContactCard;
My ContactCards
import ContactCard from "./ContactCard";
import axios from "axios";
function ContactCards() {
const[contacts, setContacts] = useState([])
useEffect(() => {
axios.get('https://texas-crm1.herokuapp.com/api/contactinfo')
.then (res =>{
console.log(res.data[0])
let data = res.data[0];
setContacts ({
contacts: [
{
thumbnail: clock,
title: "Opening Times",
data1: data.open_hour,
data2: data.close_hour,
},
{
thumbnail: telephone,
title: "Phone",
data1: data.phone_1,
data2: data.phone_2,
},
{
thumbnail: location,
title: "Location",
data1: data.street_name,
data2: data.city + ", " + data.state + ", " + data.country,
},
],
});
})
.catch(err =>{
console.log(err)
})
})
return (
<div>
</div>
)
}
Here, I got red underline from contacts in setContacts to all the way down to the closing parenthesis before catch function.
And the error says the following:
Argument of type '{ contacts: { thumbnail: string; title: string; data1: any; data2: any; }[]; }' is not assignable to parameter of type 'SetStateAction<never[]>'.
{ contacts: []} is an object, your initial state is using [] and that's an array. IOW: Your trying to use setState on something that's currently an array, into something that will become an object that contains an array called contacts.
What I would do is create an interface for you useState, based on your IContactCardProps
Something like ->
const[contacts, setContacts] =
useState<{contacts:IContactCardProps[]}>
({contacts: []});
Your default setState now is an object with the correct shape for your setContacts. The advantage here too is that your setContacts is now type checked, eg. You couldn't accidentally put t1tle: "Phone",.
Instead of giving it an array of any, initialize a typed array and initialize your useState with that:
const emptyContacts: IContactCardProps[] = [];
const [contacts, setContacts] = useState(emptyContacts);

Property 'products' does not exist on type 'Readonly<{}>'

EDIT on #Lesiak request:
Here is my getProducts call
// #api/shopifyProducts.ts
import Client from 'shopify-buy'
const client = Client.buildClient({
// TODO: add to dotenv
domain: 'some-domain.myshopify.com',
storefrontAccessToken: 'example-token-2597293846729587293875'
})
export const getProducts = async () => {
try {
const data = await client.product.fetchAll()
const products = await data.map((item) => {
return {
title: item.title,
description: item.description
// images: item.images
}
})
return products
} catch (error) {
throw new Error(`Product API fetch failed: ${error}`)
}
}
I have also refactored my component like this:
import React, { Component } from 'react'
import { getProducts } from '#api/shopifyProducts'
class TheListProducts extends Component<{}> {
constructor(props) {
super(props)
this.state = {
products: null
}
}
async componentDidMount() {
this.setState({
products: await getProducts()
})
console.log(this.state.products) ==> Error: Property 'products' does not exist on type 'Readonly<{}>'
}
render() {
return <p>Hey</p>
}
}
export default TheListProducts
Initial question:
Morning fellow developers,
I stumbled upon an issue I can't solve on my own even by doing an extensive research on the web. Since I am new to TS, I can see I don't understand in 100% what is really happening.
I have a component in React, where I async fetch content from Shopify content and I want to print it inside render function.
import React, { Component } from 'react'
import { getProducts } from '#api/shopifyProducts'
interface Product {
title: any
description: any
}
interface ListState {
products: {
[key: string]: Product | Function
}
}
class TheListProducts extends Component<{}, ListState> {
async componentDidMount() {
this.setState({
products: await getProducts()
})
}
render() {
return <p>{this.state.products}</p>
}
}
export default TheListProducts
I receive the following error:
(property) products: {
title: string;
description: string;
}[]
Type '{ title: string; description: string; }[]' is not assignable to type '{ [key: string]: Product; }'.
Index signature is missing in type '{ title: string; description: string; }[]'.ts(2322)
Here is how it looks in the browser:
I can read, and I think I understand the error, but have no clue whatsoever what to do about it.
If anyone could explain it to me in plain english it would be awesome.
Based on the web research I tried enhancing the Product interface like this:
interface Product {
title: string
description: string
[key: string]: string | number | undefined | Function
}
but it's like going in blind...
If you are using class component you should define the type of the state and props:
class App extends React.Component<MyProps, MyState> {
You can look here for more details

Categories