How do I combine ramda with recompose? - reactjs

I have this component;
const ReposGrid = R.pipe(
R.prop("repos"),
// branch(R.isEmpty, renderComponent(Loader)),
R.map(Repo),
ReposWraper
)
export default ReposGrid
This works fine but I want to render loader component if repos is empty. My branch from recompose just doesn't do anything. It doesn't show Loader neither displays repos when it's loaded. Can I apply R.ifElse here?

You might be mixing up ramda's pipe/compose and recompose's, and the arguments they take. Your chain is made up of a mix of higher order components and functions operating on props. It's best to keep the data handling in mapProps/withProps etc.
You might be able to achieve something similar to what you're after like this:
import { map, evolve, propSatisfies, isEmpty } from 'ramda'
import { compose, branch, renderComponent, mapProps, renameProp } from 'recompose'
const Repo = (repo) => <div>Repo {repo}</div>
const Loader = () => <div>Loader</div>
const RepoGridBase = props => <div>{props.children}</div>
const ReposGrid = compose(
branch(
propSatisfies(isEmpty, 'repos'),
renderComponent(Loader),
),
mapProps(evolve({
repos: map(Repo)
})),
renameProp('repos', 'children')
)(RepoGridBase)
function App() {
return (
<div className="App">
<ReposGrid repos={[]} />
<ReposGrid repos={['one', 'two']} />
</div>
);
}
(this will throw key warnings, you'd probably be better of doing the map inside of a component explicitly)
codesandbox link

Related

React context not keeping data in state

I am trying to use React context to replace my actual Redux.
I want to use this context to store an array of files.
This is a nextjs app, I do not know it can cause some issues
I have my FileContextProvider
import { createContext, useState, useEffect } from "react";
export const FileContext = createContext({});
export function FileContextProvider({ children }) {
const [files, setFiles] = useState([]);
useEffect(() => {
console.log('files', files);
}, [files])
const addLocalFiles = (newFiles, mode) => {
console.log('addLocalFiles', files);
setFiles([...files].concat(newFiles));
}
return (
<FileContext.Provider value={{
files,
addLocalFiles
}} >
{children}
</FileContext.Provider>
);
}
Then I created a global context where I want to initialize all my context, at the moment I only have the file one.
import { FileContextProvider } from "./file";
export default function Context({ children }) {
return (
<FileContextProvider>
{children}
</FileContextProvider>
);
}
Then I add the context in _app.js
import Context from '../context';
function MyApp({ Component, pageProps }) {
return (
<Context>
<Component {...pageProps} />
</Context>
)
}
export default MyApp;
In one of my child component I have
import { FileContext } from '../../context/file';
export default function TopBlock({ type, format }) {
return (
<div className="col-12 col-lg-8 offset-lg-2">
<FileContext.Consumer>
{({ files, addLocalFiles, removeFile, updateFile }) => (
<UploadBlock files={files} addLocalFiles={addLocalFiles} />
)}
</FileContext.Consumer>
</div>
);
}
Here my UploadBlock
import React, { useCallback, useRef } from 'react';
import { useDropzone } from 'react-dropzone';
export default function UploadBlock({ files, addLocalFiles }) {
const dropzoneParent = useRef(null);
function MyDropzone() {
const onDrop = useCallback(async acceptedFiles => {
addLocalFiles(acceptedFiles)
}, [])
const dropOptions = { onDrop }
const { getRootProps, getInputProps, isDragActive } = useDropzone(dropOptions)
return (
<div {...getRootProps()} ref={dropzoneParent}>
<input {...getInputProps()} />
<div>Drop Here</div>
</div>
)
}
return (
<div>
{MyDropzone()}
</div>
)
}
In my UploadBlock when I call addLocalFiles it's working, I have my files variable containing my new files but if I call a second time addLocalFiles the previous files does not exist in FileContextProvider , console.log('addLocalFiles', files); is returning an empty array.
The useEffect is only to debug, it is triggered each time addLocalFiles is called, and new files are well print.
I do not understand why files from the state became an empty array when I am calling back addLocalFiles
I have only one instance of Context and FileContext.Consumer I do not know if it makes any changes
In React, states are dependent on specific render. It means, every render will have its own states. So, by the time addLocalFiles are declared, specific files states are dependent on the render.
const addLocalFiles = (newFiles, mode) => {
console.log('addLocalFiles', files);
setFiles([...files].concat(newFiles)); // state `files` are used
}
I think the problem could be that you are wrapping addLocalFiles function with useCallback.
const onDrop = useCallback(async acceptedFiles => {
addLocalFiles(acceptedFiles)
}, []) // empty dependency, so it will always referencing same `addLocalFiles` function.
addLocalFiles will never be generated again, even if it renders. So, changed states files are not going to be applied in addLocalFiles.
If you want to prevent useless re-render, you should specifying its dependency, files.
const onDrop = useCallback(async acceptedFiles => {
addLocalFiles(acceptedFiles, mode)
}, [addLocalFiles]); // this function is dependent on `files`
Or you can just skip useCallback, if optimization is not really necessary. I found a StackOverflow question that could help you better understading.
please provide your component so we can be sure how u call the addLocalFiles method with the required parameter.
i also think there is no need for spreading [...files].concat(newfile) instead of files.concat(newfile)

Problem with wrapping whole React App (all pages) in certain functionality

I have React App consisting of few pages. LandingPage (to be clear, it is really the first page in app flow)is like this:
export const LocalLandingPage = () => {
const { Favorites } = useFavorites();
React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
}, []);
return (...here goes actual content...);
};
const LandingPage = withRouter(wrappedInLinkToSearchHOC(WithSnackBarHOC(LocalLandingPage)));
Here I call hook useFavorites which gathers some methods dealing with a specific subset of local Storage content(and dispatches actions to Redux store as well). The code above return call is checks support for local Storage and tells if some specific items are stored and in what quantity.
The story is that I have realized that if someone enters not the LandingPage but any other page of the app, the code will not be executed and support for localStorage not checked.
Besides, it is really not LandingPage business to deal with storage, so it should be removed anywhere.
My idea was to write HOC and wrap the application. So, here is this HOC (to keep things simple it is initially JS not TS) To make useFavorites work, it had to be a hook, too.
import useFavorites from '../hooks/useFavorites';
const useCheckSupportForLocalStorage = Component => {
const { Favorites } = useFavorites();
// React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
// }, []);
return props => <Component {...props} />;
};
export default useCheckSupportForLocalStorage;
Having it done, I have tried to use it on App like this
function App() {
return (
<Switch>
... here are routes...
</Switch>
);
}
export default useCheckSupportForLocalStorage(App);
It throws an error:
React Hook "useCheckSupportForLocalStorage" cannot be called at the top level.
That is truth, no doubt. So, the next idea was to create a temporary component from all routes.
function Routes() {
const Routes = useCheckSupportForLocalStorage(
<>
<Route exact path={Paths.landing} component={Awaiting(StarWars)} />
...here is the rest or outes...
</>,
);
return <Routes />;
}
export default Routes;
And use it in rewritten App like this
function App() {
return (
<Switch>
<Routes />
</Switch>
);
}
but it throws error
Routes' cannot be used as a JSX component.
Its return type '(props: any) => Element' is not a valid JSX element.
Type '(props: any) => Element' is missing the following properties from type 'ReactElement<any, any>': type, props, key
Forcing useCheckSupportForLocalStorage th have return type of ReactElement doesn't help, just leads to other error. I have checked few others options as well. What is wrong, how should it be written?
Basically I could stop using useFavorites in this hook, then it would be just a function - but it would be extreme headache.
You say
To make useFavorites work, it had to be a hook, too.
That is not true, since you have it in a component it will work just fine. And my understanding is that you want to wrap your whole app with it, so no need for the HOC. Just use it at the top of your app hierarchy.
something like
import useFavorites from '../hooks/useFavorites';
const CheckSupportForLocalStorage = ({ children }) => {
const { Favorites } = useFavorites();
React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
}, []);
return children;
};
export default CheckSupportForLocalStorage;
and
function App() {
return (
<CheckSupportForLocalStorage>
<Switch>
... here are routes...
</Switch>
</CheckSupportForLocalStorage>
);
}
export default App;

Using Persistent Layouts in NextJS with Higher Order Functions

Im using Adam Wathan's method for using persistent layouts in Next. Is there a way to get them to work with Higher Order Functions? I'm not really sure how HOFs work.
My _app.js
function MyApp({ Component, pageProps }) {
const Layout = Component.layout || (children => <>{children}</>)
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
A sample page looks like this
const Home = () => {
return (
<>
...
</>
)
}
Home.Layout = BaseLayout;
export const getServerSideProps = withAuthUserTokenSSR()()
export default withAuthUser()(Home)
If I remove the HOF the layouts work fine, otherwise I get:
Error: Objects are not valid as a React child (found: object with keys {children}). If you meant to render a collection of children, use an array instead.
You need to apply the layout component to the higher-order component itself, as it's probably wrapping your original Home component and hiding Home.layout away.
const Home = () => {
return (
<></>
)
}
const HomeWithAuth = withAuthUser()(Home)
HomeWithAuth.layout = BaseLayout;
export default HomeWithAuth
Also, make sure you use the same variable name (same casing, e.g., layout vs. Layout) in your page component and when you refer to it in _app.

Custom Admin component with custom dataprovider

I need custom Admin component to update the list of Resource components dynamically.
The documentation provides an example:
import * as React from 'react';
import { useEffect, useState } from 'react';
import { AdminContext, AdminUI, Resource, ListGuesser, useDataProvider } from 'react-admin';
function App() {
return (
<AdminContext dataProvider={myDataProvider}>
<AsyncResources />
</AdminContext>
);
}
function AsyncResources() {
const [resources, setResources] = useState([]);
const dataProvider = useDataProvider();
useEffect(() => {
// Note that the `getResources` is not provided by react-admin. You have to implement your own custom verb.
dataProvider.getResources().then(r => setResources(r));
}, []);
return (
<AdminUI>
{resources.map(resource => (
<Resource name={resource.name} key={resource.key} list={ListGuesser} />
))}
</AdminUI>
);
}
It is supposed to work with custom dataprovider that has getResources() function.
Ok. But how this function is supposed to look like? Or rather what it should return? Because tried many things and none is flying.
And also. There is a warning about React Hook useEffect missing a dependency: 'dataProvider'. Solutions online aren't usable so far. I don't think my example doesn't work because of that, but it's possible. How to fix that in that case?
So yeah, in short - how to make this provided in documentation example to work?
+++
Edit: In case anyone wondering here is how I apply my dataprovider:
import dataProvider from './dataprovider';
function App() {
return (
<AdminContext dataProvider={dataProvider}>
<AsyncResources />
</AdminContext>
);
}
And here is my get_resources function:
getResources: () => {
const url = `${apiUrl}/get_resources`;
return httpClient(url).then(
({ headers, json }) => ({
data: json["data"],
total: json["total"]
}));
},
Edit 2:
Problem maybe lies within dataprovider, since it fails with error message "TypeError: can't convert undefined to object" while trying to call "isDataProviderOptions" function from here: node_modules/ra-core/esm/dataProvider/getDataProviderCallArguments.js:19.
Or it's calling the wrong dataprovider. Not the custom one I've supplied to the AdminContext component.
Should I add the code of my custom dataprovider?

ReactJS hooks useContext issue

I'm kind of to ReactJS and I'm trying to use useContext with hooks but I'm having some trouble. I've been reading through several articles but I could not understand it.
I understand its purpose, but I can't figure out how to make it work properly. If I'm correct, the purpose is to be able to avoid passing props down to every children and be able to access values from a common provider at any depth of the component tree. This includes functions and state values. Please correct me if I'm wrong.
I've been testing with the following files. This is the ManagerContext.js file:
import { createContext } from 'react';
const fn = (t) => {
console.log(t);
}
const ctx = createContext({
title: 'This is a title',
editing: false,
fn: fn,
})
let ManagerContext = ctx;
export default ManagerContext;
Then I have the LessonManager.js file which is used in my main application:
import React from 'react';
import LessonMenu from './LessonMenu.js';
export default function LessonManager() {
return (
<LessonMenu />
)
}
And finally the LessonMenu.js:
import React from 'react';
import 'rsuite/dist/styles/rsuite.min.css';
import ManagerContext from './ManagerContext.js';
export default function LessonMenu() {
const value = React.useContext(ManagerContext);
return (
<div>
<span>{value.title}</span>
<button
onClick={()=>value.fn('ciao')}
>click</button>
<button
onClick={()=>value.title = 'new title'}
>click</button>
</div>
)
}
In the LessonMenu.js file the onClick={()=>value.fn('ciao')} works but the onClick={()=>value.title = 'new title'} doesn't re render the component.
I know something is wrong, but can someone make it a bit clearer for me?
In order for rerendering to occur, some component somewhere must call setState. Your code doesn't do that, so no rendering happens.
The setup you've done for the ManagerContext creates a default value, but that's only going to get used if you don't render any ManagerContext.Provider in your component tree. That's what you're doing now, but it's almost certainly not what you want to. You'll want to have some component near the top of your tree render a ManagerContext.Provider. This component can will be where the state lives, and among the data it sends down will be a function or functions which set state, thus triggering rerendering:
export default function LessonManager() {
const [title, setTitle] = useState('SomeOtherTitle');
const [editing, setEditing] = useState(false);
const value = useMemo(() => {
return {
title,
setTitle,
editing,
setEditing,
log: (t) => console.log(t)
}
}, [title, editing]);
return (
<ManagerContext.Provider value={value} >
<LessonMenu />
</ManagerContext.Provider/>
)
}
// used like:
export default function LessonMenu() {
const value = React.useContext(ManagerContext);
return (
<div>
<span>{value.title}</span>
<button onClick={() => value.log('ciao')}>
click
</button>
<button onClick={() => value.setTitle('new title')}>
click
</button>
</div>
)
}

Resources