I work on local environment and I use Next like framework. I use flexmonster component for react (https://www.npmjs.com/package/react-flexmonster)
When I make some modifications the current flexmonster component shows me this error : this.TD[this.j5] is null
I know that Flexmonster works on CSR (client side rendering) and I used a custom debounce hook to wait before excute any flexmonster functions .
Flexmonster component code :
import { useRef } from 'react';
import dynamic from 'next/dynamic';
import useDebounce from '#hooks/useDebounce';
import 'flexmonster/flexmonster.css';
const DynamicFlexMonster = dynamic(() => import('react-flexmonster'), {
ssr: false,
});
const Flexmonster = ({
dataSource = [],
rows = [],
columns = [],
measures = [],
formats = {},
viewType,
gridType,
chartType,
}) => {
const flexmonsterDataStructure = {
dataSource: {
data: dataSource,
},
slice: {
rows,
columns,
measures,
},
options: {
viewType,
grid: {
type: gridType,
showHeader: false,
showTotals: false,
showGrandTotals: 'off',
},
chart: {
type: chartType,
},
showEmptyData: true,
},
formats,
};
const ref = useRef(null);
const [debounceReport, setDebounceReport] = useDebounce(null);
const onReportComplete = () => {
setDebounceReport(ref.current, 1000);
if (debounceReport) {
console.log('>>>>', ref.current.flexmonster.getReport());
}
};
return (
<>
<DynamicFlexMonster
ref={ref}
toolbar={false}
width="100%"
report={flexmonsterDataStructure}
reportcomplete={onReportComplete}
/>
</>
);
};
export default Flexmonster;
I was facing the same issue, the main problem is Flexmonster use the window element so when you are working with an SSR framework like Nextjs you will need to call the all page where you display your Flexmonster component as SSR: false.
From your code, you will have to call your Flexmonster component like this:
/* pages/my-flexmonster-component-page */
import dynamic from 'next/dynamic';
const Flexmonster = dynamic(() => import('#components/Flexmonster'), {
ssr: false,
});
export default function MyFlexmonsterComponentPage(){
return (
<>
<Flexmonster/>
</>
);
}
Related
I was trying to create Lightweight Charts in Next.js. Firstly, I created chart component which accepts chart data as props.
import { createChart, CrosshairMode } from 'lightweight-charts';
import { useEffect, useRef } from 'react';
const CandleStickChart = ({ OHLC_Data }) => {
const chartContainerRef = useRef(null);
useEffect(() => {
const chart = createChart(chartContainerRef.current, {
width: 700,
height: 400,
timeScale: {
timeVisible: true,
secondsVisible: false,
fixLeftEdge: true,
fixRightEdge: true,
},
crosshair: {
mode: CrosshairMode.Normal,
},
});
const candleSeries = chart.addCandlestickSeries({
priceFormat: {
type: 'price',
minMove: 0.00001,
precision: 5,
},
});
candleSeries.setData(OHLC_Data);
}, [OHLC_Data]);
return <div ref={chartContainerRef} />;
};
export default CandleStickChart;
In page component, I used getServerSideProps to fetch the data required for chart.
import { useCallback, useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import Image from 'next/image';
const CandleStickChart = dynamic(
() => import('../../components/CandleStickChart'),
{ ssr: false }
);
import { fetchTimeSeries } from '../../utils/fetchData';
const ForexPair = ({ meta, hour, day, week }) => {
const { currency_base, currency_quote, symbol, type } = meta;
const [logos, setLogos] = useState(null);
return (
<>
<CandleStickChart OHLC_Data={hour.values} />
<CandleStickChart OHLC_Data={day.values} />
<CandleStickChart OHLC_Data={week.values} />
</>
);
};
export default ForexPair;
export async function getServerSideProps(context) {
const { params, res } = context;
const { pairSymbol } = params;
res.setHeader(
'Cache-Control',
'public, s-maxage=300, stale-while-revalidate=3600'
);
const { meta, hour, day, week } = await fetchTimeSeries(
pairSymbol.replace('-', '/')
);
return {
props: {
meta,
hour,
day,
week,
},
};
}
I excluded some unnecessary codes. fetchTimeSeries function is just asynchronous function to fetch data for third party api. getServerSideProps is returning data format required in <CandleStickChart /> component.
What I expect to see is total 3 charts since I only included 3 components. but I am seeing two of each charts, total of 6 charts.
Please dont yell at me if my code looks terrible, I am new to Next.js :D. I really have no idea what is happening in my app.
p.s Charts are displaying correctly, just seeing duplicate charts for each. Thanks in advance. :D
It has to do with the way React & useEffect works under the hood.
Just add
return () => {
chart.remove();
};
after candleSeries.setData(OHLC_Data); and you won't see duplications :)
I'm not able to get dynamic import to work,
//event.js
import { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic";
const NoSsrLiveStreaming = dynamic(
() => import("#/components/ui/LiveStreaming"),
{
ssr: false
}
);
async function Event() {
return (
<>
<div suppressHydrationWarning={true}>
<NoSsrLiveStreaming />
</div>
</>
);
}
export default Event;
component:
//LiveStreaming.js
function LiveStreaming() {
const [appid, setAppid] = useState("");
const [token, setToken] = useState("");
const [channel, setChannel] = useState("");
let client, agoraObj;
if (isBrowser) {
client = AgoraRTC.createClient({ codec: "h264", mode: "rtc" });
agoraObj = isBrowser() ? useAgora(client) : {};
}
const {
localAudioTrack,
localVideoTrack,
leave,
join,
joinState,
remoteUsers
} = isBrowser
? agoraObj
: {
localAudioTrack: null,
localVideoTrack: null,
leave: null,
join: null,
joinState: null,
remoteUsers: null
};
It seems like it's loading the hook useLiveStream even though I disabled SSR with dynamic import.
I do have a hook in this component useAgora(client), and this hook has window objects that threw errors. That's the reason why I wrapped this around an if browser check statement.
How do i get around this?
Here my redux state , the state has dynamic nested object name
const search = {
client :
{ result: [],
selected: null,
isLoading: false,
isSuccess: false,},
[dynamicKey] :
{ result: [],
selected: null,
isLoading: false,
isSuccess: false,},
[dynamicKey2] :
{ result: [],
selected: null,
isLoading: false,
isSuccess: false,}
};
I'm trying to get nested object by dynamic key , here is my selector code :
import { createSelector } from "reselect";
export const searchState = (state) => state.search;
export const selectSearch = (keyRef) =>
createSelector([searchState], (search) => search[keyRef]);
You forgot to ask the question but your code looks fine as it is. In the component you can use useMemo to not needlessly create the selector:
//renamed the selector to create...
export const createSelectSearch = (keyRef) =>
createSelector([searchState], (search) => search[keyRef]);
//compnent example
const MyComponent = ({keyRef}) => {
const selectSearch = React.useMemo(
()=>createSelector(keyRef),//create the selector when keyRef changes
[keyRef]
);
const result = useSelector(selectSearch)
return <jsx />
}
Some more information about this pattern can be found here
I have an issue where I have a simple React.Context that's populated after all the components mount. The problem is that because it happens after mount, nextjs does not see this data on initial render, and so there's noticeable flicker.
Here's the simple component that sets the Context:
export const SetTableOfContents = (props: { item: TableOfContentsItem }) => {
const toc = useContext(TableOfContentsContext);
useEffect(() => {
// Updates the React.Context after the component mount
// (since useEffects run after mount)
toc.setItem(props.item);
}, [props.item, toc]);
return null;
};
Here's the React.Context. It uses React state to store the TOC items.
export const TableOfContentsProvider = (props: {
children?: React.ReactNode;
}) => {
const [items, setItems] = useState<TableOfContents["items"]>([]);
const value = useMemo(() => {
return {
items,
setItem(item: TableOfContentsItem) {
setItems((items) => items.concat(item));
},
};
}, [items]);
return (
<TableOfContentsContext.Provider value={value}>
{props.children}
</TableOfContentsContext.Provider>
);
};
Currently, it is not possible to set the React.Context before mount because React gives a warning---Cannot update state while render.
The only workaround I can think of is to use something other than React.state for the React.Context state---that way the component can update it any time it wants. But then the problem with that approach is that Context Consumers would no longer know that the items changed (because updates live outside the React lifecycle)!
So how to get the initial React.Context into the initial SSR render?
const items = [];
export const TableOfContentsProvider = (props: {
children?: React.ReactNode;
}) => {
const value = useMemo(() => {
return {
items,
setItem(item: TableOfContentsItem) {
items[item.index] = item;
},
};
// this dep never changes.
// when you call this function, values never change
}, [items]);
return (
<TableOfContentsContext.Provider value={value}>
{props.children}
</TableOfContentsContext.Provider>
);
};
Here's what I ended up doing:
render the app in getStaticProps using renderToString
use useRef for state in the Context instead of useState
the reason for doing this is because renderToString renders only the initial state. So if you update the Context using useState, it won't capture subsequent renders
update the Context on component initialization for the reason mentioned above
pass the Context an "escape hatch"---a function we can call to get the state calculated on the initial render
Yes, the whole thing seems like a giant hack! :-) I'm not sure if React.Context plays well with SSR :(
export const TableOfContentsProvider = (props: {
initialItems?: TableOfContentsItem[];
setItemsForSSR?: (items: TableOfContentsItem[]) => void;
children?: React.ReactNode;
}) => {
// use useRef for the reasons mentioned above
const items = useRef(props.initialItems || []);
// Client still needs to see updates, so that's what this is for
const [count, setCount] = useState(0);
const { setItemsForSSR } = props;
const setterValue = useMemo(
() => ({
setItem(item: TableOfContentsItem) {
if (!items.current.find((x) => x.id === item.id)) {
items.current.push(item);
items.current.sort((a, b) => a.index - b.index);
setCount((count) => count + 1);
setItemsForSSR?.(items.current);
}
},
}),
[setItemsForSSR]
);
const stateValue = useMemo(() => ({ items: items.current, count }), [count]);
return (
<TableOfContentsSetterContext.Provider value={setterValue}>
<TableOfContentsStateContext.Provider value={stateValue}>
{props.children}
</TableOfContentsStateContext.Provider>
</TableOfContentsSetterContext.Provider>
);
};
interface TableOfContentsSetterWorkerProps {
item: TableOfContentsItem;
setItem: (item: TableOfContentsItem) => void;
}
export class TableOfContentsSetterWorker extends React.Component<
TableOfContentsSetterWorkerProps,
{}
> {
constructor(props: TableOfContentsSetterWorkerProps) {
super(props);
// Need to do this on init otherwise renderToString won't record it
props.setItem(props.item);
}
render() {
return null;
}
}
/**
* Usage: use this as a child component when the parent needs to set the TOC.
*
* Exists so that a component can set the TOC without triggering
* an unnecessary render on itself.
*/
export function TableOfContentsSetter(props: { item: TableOfContentsItem }) {
const { setItem } = useContext(TableOfContentsSetterContext);
return <TableOfContentsSetterWorker item={props.item} setItem={setItem} />;
export const getStaticProps = async () => {
let initialTableOfContents: TableOfContentsItem[] = [];
const getItems = (items: TableOfContentsItem[]) => {
initialTableOfContents = [...items];
};
const app = () => (
<TableOfContentsProvider setItemsForSSR={getItems}>
<AppArticles />
</TableOfContentsProvider>
);
renderToString(app());
return {
props: {
initialTableOfContents,
},
};
};
I'm having issues getting complete coverage on my apollo components. istanbul is reporting the functions inside compose() are not getting called. These are Redux connect() functions and apollo graph() functions.
export default compose (
...
connect(mapStateToProps, mapDispatchToProps), // <-- functions not covered
graphql(builderQuery, {
options: (ownProps) => { // <-- function not covered
...
)(ComponentName);
I'm mounting using enzyme, trying to do something similar to the react-apollo example.
const mounted = shallow(
<MockedProvider mocks={[
{ request: { query, variables }, result: { data: response.data } }
]}>
<ConnectedComponentName />
</MockedProvider>
);
The only way I've been able to achieve 100% coverage is if I export all of the functions and call them directly.
testing composed graphql/redux containers
Try something like this for your setup:
// mocks.js
import configureMockStore from 'redux-mock-store'
import { ApolloClient } from 'react-apollo'
import { mockNetworkInterface } from 'apollo-test-utils'
export const mockApolloClient = new ApolloClient({
networkInterface: mockNetworkInterface(),
})
export const createMockStore = configureMockStore()
This will set you up to properly test your containers:
// container-test.js
import { mount } from 'enzyme'
import { createMockStore, mockApolloClient } from 'mocks'
beforeEach(() => {
store = createMockStore(initialState)
wrapper = mount(
<ApolloProvider client={mockApolloClient} store={store}>
<Container />
</ApolloProvider>
)
})
it('maps state & dispatch to props', () => {
const props = wrapper.find('SearchResults').props()
const expected = expect.arrayContaining([
// These props come from an HOC returning my grapqhql composition
'selectedListing',
'selectedPin',
'pathname',
'query',
'bbox',
'pageNumber',
'locationSlug',
'selectListing',
'updateCriteria',
'selectPin',
])
const actual = Object.keys(props)
expect(actual).toEqual(expected)
})
testing graphql options
Because the graphql fn has a signature like graphql(query, config), you can export your config for testing in isolation for more granular coverage.
import { config } from '../someQuery/config'
describe('config.options', () => {
const state = {
bbox: [],
locationSlug: 'foo',
priceRange: 'bar',
refinements: 'baz',
userSort: 'buzz',
}
const results = {
points: [
{ propertyName: 'Foo' },
{ propertyName: 'Bar' },
],
properties: [
{ propertyName: 'Foo' },
{ propertyName: 'Bar' },
],
}
it('maps input to variables', () => {
const { variables } = config.options(state)
const expected = { bbox: [], locationSlug: 'foo', priceRange: 'bar', refinements: 'baz', userSort: 'buzz' }
expect(variables).toEqual(expected)
})
it('returns props', () => {
const response = { data: { loading: false, geo: { results } } }
const props = config.props(response)
expect(props.results).toEqual(results.properties)
expect(props.spotlightPoints).toEqual(results.points)
})
})